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