You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

161 lines
5.6 KiB

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})