import csv import io import unicodedata from django.contrib import admin, messages from django.urls import path from django.shortcuts import render, redirect from django import forms from .models import Course, Arrivee, Coureur def _normalize_csv_header(header): return ''.join( character for character in unicodedata.normalize('NFKD', header).encode('ascii', 'ignore').decode('ascii').lower() if character.isalnum() ) def _pronote_csv_reader(content): first_line = content.splitlines()[0] if content.splitlines() else '' try: dialect = csv.Sniffer().sniff(first_line, delimiters='\t;,') delimiter = dialect.delimiter except csv.Error: delimiter = max(('\t', ';', ','), key=first_line.count) reader = csv.reader(io.StringIO(content, newline=''), delimiter=delimiter) headers = next(reader, []) columns = {_normalize_csv_header(header): index for index, header in enumerate(headers)} return reader, columns def _split_pronote_student(student): parts = student.split() for index, part in enumerate(parts): if any(character.islower() for character in part): return ' '.join(parts[:index]), ' '.join(parts[index:]) return None, None @admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ('nom', 'date', 'type', 'owner', 'depart', 'fin') search_fields = ('nom', 'owner__username') list_filter = ('date', 'type') fields = ('nom', 'date', 'type', 'owner', 'depart', 'fin') @admin.register(Arrivee) class ArriveeAdmin(admin.ModelAdmin): list_display = ('course', 'coureur', 'temps', 'rang', 'date_arrivee') search_fields = ('course__nom', 'coureur__nom') list_filter = ('course',) @admin.register(Coureur) class CoureurAdmin(admin.ModelAdmin): list_display = ('id', 'nom', 'prenom', 'classe') search_fields = ('nom', 'prenom', 'classe') list_filter = ('classe',) change_list_template = "admin/main/coureur_changelist.html" def get_urls(self): urls = super().get_urls() custom_urls = [ path('import-csv/', self.admin_site.admin_view(self.import_csv), name='main_coureur_import_csv'), path('import-pronote/', self.admin_site.admin_view(self.import_pronote), name='main_coureur_import_pronote'), path('clear-all/', self.admin_site.admin_view(self.clear_all), name='main_coureur_clear_all'), ] return custom_urls + urls def clear_all(self, request): if request.method == 'POST': coureur_count = Coureur.objects.count() arrivee_count = Arrivee.objects.count() Coureur.objects.all().delete() self.message_user( request, f"Suppression terminée : {coureur_count} coureur(s) et {arrivee_count} arrivée(s) supprimé(s).", messages.SUCCESS, ) return redirect('..') return render(request, "admin/main/clear_coureurs.html", { "coureur_count": Coureur.objects.count(), "arrivee_count": Arrivee.objects.count(), }) class CsvImportForm(forms.Form): csv_file = forms.FileField(label="Fichier CSV (nom;prenom;classe)") def import_csv(self, request): if request.method == "POST": form = self.CsvImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = form.cleaned_data['csv_file'] imported = 0 ignored = 0 for line in csv_file.read().decode('utf-8').splitlines(): parts = [p.strip() for p in line.split(';')] if len(parts) != 3: continue nom, prenom, classe = parts classe = classe.replace(' ', '') if Coureur.objects.filter(nom=nom, prenom=prenom, classe=classe).exists(): ignored += 1 continue Coureur.objects.create(nom=nom, prenom=prenom, classe=classe) imported += 1 self.message_user(request, f"Import terminé : {imported} ajoutés, {ignored} ignorés (doublons).", messages.SUCCESS) return redirect("..") else: form = self.CsvImportForm() return render(request, "admin/main/import_csv.html", {"form": form}) class PronoteImportForm(forms.Form): csv_file = forms.FileField(label="Fichier généré via client Pronote (.txt ou .csv)") def clean_csv_file(self): csv_file = self.cleaned_data['csv_file'] if not csv_file.name.lower().endswith(('.txt', '.csv')): raise forms.ValidationError("Le fichier doit être au format .txt ou .csv.") return csv_file def import_pronote(self, request): form = self.PronoteImportForm(request.POST or None, request.FILES or None) if request.method == 'POST' and form.is_valid(): try: content = form.cleaned_data['csv_file'].read().decode('utf-8-sig') reader, columns = _pronote_csv_reader(content) except (UnicodeDecodeError, csv.Error, IndexError): form.add_error('csv_file', "Le fichier n'a pas pu être lu comme un export CSV Pronote UTF-8.") else: if 'eleve' not in columns or 'classe' not in columns: form.add_error('csv_file', "Les colonnes « Élève » et « Classe » sont obligatoires.") else: imported = 0 ignored = 0 for row in reader: if max(columns['eleve'], columns['classe']) >= len(row): ignored += 1 continue nom, prenom = _split_pronote_student(row[columns['eleve']].strip()) classe = row[columns['classe']].strip().replace(' ', '') if not nom or not prenom or not classe: ignored += 1 continue if Coureur.objects.filter(nom=nom, prenom=prenom, classe=classe).exists(): ignored += 1 continue Coureur.objects.create(nom=nom, prenom=prenom, classe=classe) imported += 1 self.message_user( request, f"Import Pronote terminé : {imported} ajoutés, {ignored} ignorés (doublons ou lignes invalides).", messages.SUCCESS, ) return redirect('..') return render(request, "admin/main/import_pronote.html", {"form": form})