commit
e1a68eb65c
22 changed files with 1120 additions and 0 deletions
@ -0,0 +1,13 @@ |
|||||||
|
from django.core.asgi import get_asgi_application |
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter |
||||||
|
from channels.auth import AuthMiddlewareStack |
||||||
|
import main.routing |
||||||
|
|
||||||
|
application = ProtocolTypeRouter({ |
||||||
|
"http": get_asgi_application(), |
||||||
|
"websocket": AuthMiddlewareStack( |
||||||
|
URLRouter( |
||||||
|
main.routing.websocket_urlpatterns |
||||||
|
) |
||||||
|
), |
||||||
|
}) |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
""" |
||||||
|
Django settings for IAProf project. |
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.x. |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'. |
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent |
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production |
||||||
|
# See https://docs.djangoproject.com/en/4.x/howto/deployment/checklist/ |
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret! |
||||||
|
SECRET_KEY = 'your-secret-key' |
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production! |
||||||
|
DEBUG = True |
||||||
|
|
||||||
|
ALLOWED_HOSTS = [] |
||||||
|
|
||||||
|
# Application definition |
||||||
|
|
||||||
|
INSTALLED_APPS = [ |
||||||
|
'django.contrib.admin', |
||||||
|
'django.contrib.auth', |
||||||
|
'django.contrib.contenttypes', |
||||||
|
'django.contrib.sessions', |
||||||
|
'django.contrib.messages', |
||||||
|
'django.contrib.staticfiles', |
||||||
|
'main', # Your main application |
||||||
|
] |
||||||
|
|
||||||
|
MIDDLEWARE = [ |
||||||
|
'django.middleware.security.SecurityMiddleware', |
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware', |
||||||
|
'django.middleware.common.CommonMiddleware', |
||||||
|
'django.middleware.csrf.CsrfViewMiddleware', |
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware', |
||||||
|
'django.contrib.messages.middleware.MessageMiddleware', |
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
||||||
|
] |
||||||
|
|
||||||
|
ROOT_URLCONF = 'ia_prof.urls' |
||||||
|
|
||||||
|
TEMPLATES = [ |
||||||
|
{ |
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
||||||
|
'DIRS': [os.path.join(BASE_DIR, 'main/templates')], |
||||||
|
'APP_DIRS': True, |
||||||
|
'OPTIONS': { |
||||||
|
'context_processors': [ |
||||||
|
'django.template.context_processors.debug', |
||||||
|
'django.template.context_processors.request', |
||||||
|
'django.contrib.auth.context_processors.auth', |
||||||
|
'django.contrib.messages.context_processors.messages', |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
WSGI_APPLICATION = 'ia_prof.wsgi.application' |
||||||
|
|
||||||
|
# Database |
||||||
|
# https://docs.djangoproject.com/en/4.x/ref/settings/#databases |
||||||
|
|
||||||
|
DATABASES = { |
||||||
|
'default': { |
||||||
|
'ENGINE': 'django.db.backends.sqlite3', |
||||||
|
'NAME': BASE_DIR / 'db.sqlite3', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# Password validation |
||||||
|
# https://docs.djangoproject.com/en/4.x/ref/settings/#auth-password-validators |
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [ |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
# Internationalization |
||||||
|
# https://docs.djangoproject.com/en/4.x/topics/i18n/ |
||||||
|
|
||||||
|
LANGUAGE_CODE = 'fr' |
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Paris' |
||||||
|
|
||||||
|
USE_I18N = True |
||||||
|
|
||||||
|
USE_L10N = True |
||||||
|
|
||||||
|
USE_TZ = True |
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images) |
||||||
|
# https://docs.djangoproject.com/en/4.x/howto/static-files/ |
||||||
|
|
||||||
|
STATIC_URL = '/static/' |
||||||
|
|
||||||
|
# Default primary key field type |
||||||
|
# https://docs.djangoproject.com/en/4.x/ref/settings/#default-auto-field |
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' |
||||||
|
OPENAI_API_KEY = "sk-proj-hrV9Se3D3Vn6ro66AoMFT3BlbkFJ3kgB6P9xQFpcaymQQHFI" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
from django.urls import path, include, re_path |
||||||
|
from django.views.generic.base import RedirectView |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('admin/', admin.site.urls), |
||||||
|
re_path(r'^$', RedirectView.as_view(url='/login/', permanent=False)), |
||||||
|
path('', include('main.urls')), |
||||||
|
] |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
""" |
||||||
|
WSGI config for IAProf project. |
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named `application`. |
||||||
|
|
||||||
|
For more information on this file, see |
||||||
|
https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/ |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
from django.core.wsgi import get_wsgi_application |
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ia_prof.settings') |
||||||
|
|
||||||
|
application = get_wsgi_application() |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
from .models import UserProfile |
||||||
|
|
||||||
|
admin.site.register(UserProfile) |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
class MainConfig(AppConfig): |
||||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||||
|
name = 'main' |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer |
||||||
|
import json |
||||||
|
|
||||||
|
class ChatConsumer(AsyncWebsocketConsumer): |
||||||
|
async def connect(self): |
||||||
|
await self.accept() |
||||||
|
|
||||||
|
async def disconnect(self, close_code): |
||||||
|
pass |
||||||
|
|
||||||
|
async def receive(self, text_data): |
||||||
|
text_data_json = json.loads(text_data) |
||||||
|
message = text_data_json['message'] |
||||||
|
|
||||||
|
await self.send(text_data=json.dumps({ |
||||||
|
'message': message |
||||||
|
})) |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
from django.db import models |
||||||
|
from django.contrib.auth.models import User |
||||||
|
|
||||||
|
class UserProfile(models.Model): |
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE) |
||||||
|
# Ajoutez d'autres champs personnalisés ici si besoin |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.user.username |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
from json import dumps |
||||||
|
import pdfplumber |
||||||
|
import openai |
||||||
|
from PyPDF2 import PdfReader |
||||||
|
from django.conf import settings |
||||||
|
|
||||||
|
openai.api_key = settings.OPENAI_API_KEY |
||||||
|
|
||||||
|
def convertPdfToJSON(file): |
||||||
|
eleve_id = 1 |
||||||
|
eleves = [] |
||||||
|
|
||||||
|
pdf = pdfplumber.open(file) |
||||||
|
for page in pdf.pages: |
||||||
|
|
||||||
|
curent_page = [] |
||||||
|
write = False |
||||||
|
eleve_found = False |
||||||
|
app_gen = "" |
||||||
|
|
||||||
|
current_eleve = {"eleve_id": "", |
||||||
|
"eleve": "", |
||||||
|
"app_generale": "", |
||||||
|
"appreciations": []} |
||||||
|
|
||||||
|
# lecture ligne par ligne |
||||||
|
lines = page.extract_text().split('\n') |
||||||
|
for i, line in enumerate(lines): |
||||||
|
|
||||||
|
# Attendre la ligne qui se termine par " Trimestre" |
||||||
|
if not write: |
||||||
|
if line.strip().endswith("Trimestre"): |
||||||
|
write = True |
||||||
|
continue |
||||||
|
|
||||||
|
# Enregistrer la ligne suivante (nom de l'élève) |
||||||
|
if write and not eleve_found: |
||||||
|
eleve_nom = line.strip() |
||||||
|
# Vérifier si l'élève n'est pas déjà dans la liste et que le bulletin ne tient pas sur plusieurs pages |
||||||
|
if not any(e["eleve"] == eleve_nom for e in eleves) and not line.strip().endswith("élèves)"): |
||||||
|
current_eleve["eleve_id"] = f"ELEVE{eleve_id}" |
||||||
|
current_eleve["eleve"] = eleve_nom |
||||||
|
eleve_id += 1 |
||||||
|
eleve_found = True |
||||||
|
else: |
||||||
|
# Si l'élève est déjà trouvé, on prend le dernier élève de la liste auquel on va ajouter les données de la nouvelle page |
||||||
|
current_eleve = eleves.pop() |
||||||
|
eleve_found = True |
||||||
|
continue |
||||||
|
|
||||||
|
# Si on a trouvé l'élève, on cherche l'appréciation générale |
||||||
|
if eleve_found and line.strip().startswith("Appréciation globale :"): |
||||||
|
app_gen = line.strip().split(":", 1)[-1].strip() |
||||||
|
continue |
||||||
|
|
||||||
|
# Si on a trouvé l'appréciation générale, on continue à la lire jusqu'à la ligne "Le Chef d'établissement" |
||||||
|
if app_gen: |
||||||
|
if not line.strip().startswith("Le Chef d'établissement"): |
||||||
|
app_gen += line.strip()+" " |
||||||
|
else: |
||||||
|
current_eleve["app_generale"] = app_gen |
||||||
|
continue |
||||||
|
|
||||||
|
# récupération des tableaux |
||||||
|
tables = page.extract_tables() |
||||||
|
tables = tables[0][2:] |
||||||
|
for table in tables: |
||||||
|
if table[1] != None and table[-1] != None: |
||||||
|
current_eleve["appreciations"].append({"matiere": table[1].split('\n')[0], "appreciation": table[-1]}) |
||||||
|
|
||||||
|
eleves.append(current_eleve) |
||||||
|
return eleves |
||||||
|
|
||||||
|
def generer_appreciation_pour_eleve(eleve, eleves_json): |
||||||
|
eleve_data = next((e for e in eleves_json if e["eleve"] == eleve), None) |
||||||
|
if not eleve_data: |
||||||
|
return f"Aucun élève trouvé avec le nom {eleve}" |
||||||
|
|
||||||
|
completion = openai.ChatCompletion.create( |
||||||
|
model="ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON", |
||||||
|
messages=[ |
||||||
|
{"role": "system", "content": "Rédige une appréciation générale (500 caractères max) pour cet élève"}, |
||||||
|
{"role": "user", "content": dumps(eleve_data["appreciations"])} |
||||||
|
], |
||||||
|
temperature=0.7, |
||||||
|
presence_penalty=0.6, |
||||||
|
frequency_penalty=0.6, |
||||||
|
top_p=0.5) |
||||||
|
|
||||||
|
return completion.choices[0].message.content |
||||||
@ -0,0 +1,129 @@ |
|||||||
|
from openai import OpenAI |
||||||
|
from PyPDF2 import PdfReader |
||||||
|
from django.conf import settings |
||||||
|
|
||||||
|
liste_eleves = {} |
||||||
|
client = OpenAI(api_key=settings.OPENAI_API_KEY) |
||||||
|
|
||||||
|
def split_by_eleve(text): |
||||||
|
""" |
||||||
|
Découpe le texte en une liste de chaînes, chaque élément commençant par 'ELEVE'. |
||||||
|
""" |
||||||
|
import re |
||||||
|
# On split sur chaque occurrence de 'ELEVE' suivie d'un ou plusieurs chiffres |
||||||
|
parts = re.split(r'(ELEVE\d+)', text) |
||||||
|
result = [] |
||||||
|
for i in range(1, len(parts), 2): |
||||||
|
# parts[i] est 'ELEVEid', parts[i+1] est le texte associé |
||||||
|
bloc = parts[i] + parts[i+1] if i+1 < len(parts) else parts[i] |
||||||
|
result.append(bloc.strip()) |
||||||
|
return result |
||||||
|
|
||||||
|
def anonymiserPdf(file, eleves): |
||||||
|
eleve_id = 1 |
||||||
|
pages_content = "" |
||||||
|
|
||||||
|
for page in file.pages: |
||||||
|
lines = page.extract_text().split('\n') |
||||||
|
curent_page = [] |
||||||
|
write = False |
||||||
|
eleve_found = False |
||||||
|
|
||||||
|
for i, line in enumerate(lines): |
||||||
|
# Attendre la ligne qui se termine par " Trimestre" |
||||||
|
if not write: |
||||||
|
if line.strip().endswith("Trimestre"): |
||||||
|
write = True |
||||||
|
continue |
||||||
|
|
||||||
|
# Enregistrer la ligne suivante (nom de l'élève) |
||||||
|
if write and not eleve_found: |
||||||
|
eleve_nom = line.strip() |
||||||
|
if eleve_nom not in eleves: |
||||||
|
eleves[eleve_nom] = f"ELEVE{eleve_id}" |
||||||
|
eleve_id += 1 |
||||||
|
curent_page.append(eleves[eleve_nom] + "\n") |
||||||
|
eleve_found = True |
||||||
|
continue |
||||||
|
|
||||||
|
# Attendre la ligne qui commence par "Appréciations" |
||||||
|
if eleve_found: |
||||||
|
if line.strip().startswith("Appréciations"): |
||||||
|
# Commencer à écrire les lignes suivantes |
||||||
|
for l in lines[i+1:]: |
||||||
|
if l.startswith("M.") or l.startswith("Mme"): |
||||||
|
continue |
||||||
|
curent_page.append(l + "\n") |
||||||
|
break # Fin du traitement de cette page |
||||||
|
|
||||||
|
pages_content += "".join(curent_page) |
||||||
|
return pages_content |
||||||
|
|
||||||
|
def get_eleve_name(text, eleves): |
||||||
|
""" |
||||||
|
Extrait le nom de l'élève à partir du texte. |
||||||
|
""" |
||||||
|
for line in text.split('\n'): |
||||||
|
line = line.strip() |
||||||
|
if line in eleves: |
||||||
|
return eleves[line] |
||||||
|
return None |
||||||
|
|
||||||
|
def replace_eleve_ids_with_names(text, eleves): |
||||||
|
# Inverse the eleves dict to map ELEVEid -> nom prénom |
||||||
|
id_to_name = {v: k for k, v in eleves.items()} |
||||||
|
# Trier les IDs par longueur décroissante pour éviter les remplacements partiels |
||||||
|
for eleve_id in sorted(id_to_name.keys(), key=len, reverse=True): |
||||||
|
nom = id_to_name[eleve_id] |
||||||
|
if eleve_id in text: |
||||||
|
text = text.replace(eleve_id, nom) |
||||||
|
return text |
||||||
|
|
||||||
|
def generatationAppreciations(appreciations_path, modele_path=None): |
||||||
|
|
||||||
|
if (modele_path): |
||||||
|
modele = PdfReader(modele_path) |
||||||
|
modele_anonyme = anonymiserPdf(modele, liste_eleves) |
||||||
|
|
||||||
|
appreciations = PdfReader(appreciations_path) |
||||||
|
appreciation_anonyme = anonymiserPdf(appreciations, liste_eleves) |
||||||
|
tableau_appreciations = split_by_eleve(appreciation_anonyme) |
||||||
|
|
||||||
|
responses = [] |
||||||
|
#debug |
||||||
|
tableau_appreciations = tableau_appreciations[:3] |
||||||
|
for eleve in tableau_appreciations: |
||||||
|
if modele_path: |
||||||
|
# Si un modèle est fourni, on utilise le modèle pour chaque élève |
||||||
|
data = [ |
||||||
|
{ |
||||||
|
"role": "developer", |
||||||
|
"content": "Tu dois générer des appréciations générales de bulletins avec le style de l'exemple suivant (500 caractères max)" |
||||||
|
},{ |
||||||
|
"role": "assistant", |
||||||
|
"content": modele_anonyme |
||||||
|
}, |
||||||
|
{ |
||||||
|
"role": "user", |
||||||
|
"content": eleve |
||||||
|
}] |
||||||
|
else: |
||||||
|
data = [ |
||||||
|
{ |
||||||
|
"role": "developer", |
||||||
|
"content": "Tu dois générer des appréciations générales de bulletins (500 caractères max)" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"role": "user", |
||||||
|
"content": eleve |
||||||
|
}] |
||||||
|
completion = client.chat.completions.create( |
||||||
|
model="gpt-4.1-mini", |
||||||
|
messages=data, |
||||||
|
temperature=1, |
||||||
|
top_p=1) |
||||||
|
resultat = "<b><u><i>" + get_eleve_name(completion.choices[0].message.content, liste_eleves) + "</i></u></b>\n" |
||||||
|
resultat += replace_eleve_ids_with_names(completion.choices[0].message.content, liste_eleves) |
||||||
|
responses.append(resultat) |
||||||
|
|
||||||
|
return responses |
||||||
@ -0,0 +1,136 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="fr"> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||||
|
<title>IAProf</title> |
||||||
|
<!-- SB Admin Bootstrap CSS --> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/startbootstrap-sb-admin@7.0.6/dist/css/styles.min.css" rel="stylesheet"> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||||
|
<style> |
||||||
|
:root { |
||||||
|
color-scheme: light dark; |
||||||
|
} |
||||||
|
body, .bg-light { |
||||||
|
background-color: #f8f9fa; |
||||||
|
color: #212529; |
||||||
|
} |
||||||
|
@media (prefers-color-scheme: dark) { |
||||||
|
html, body, .bg-light { |
||||||
|
background-color: #181a1b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
.card, .card-header, .card-body { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
.table { |
||||||
|
color: #f8f9fa; |
||||||
|
background-color: #23272b; |
||||||
|
} |
||||||
|
.table-bordered th, .table-bordered td { |
||||||
|
border-color: #444c56; |
||||||
|
} |
||||||
|
.form-select, .form-label, .btn, .alert { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
.btn-primary { |
||||||
|
background-color: #375a7f !important; |
||||||
|
border-color: #375a7f !important; |
||||||
|
} |
||||||
|
.btn-danger { |
||||||
|
background-color: #c9302c !important; |
||||||
|
border-color: #c9302c !important; |
||||||
|
} |
||||||
|
.btn-success { |
||||||
|
background-color: #449d44 !important; |
||||||
|
border-color: #449d44 !important; |
||||||
|
} |
||||||
|
.navbar, .navbar-dark, .bg-dark { |
||||||
|
background-color: #23272b !important; |
||||||
|
} |
||||||
|
.alert-warning { |
||||||
|
background-color: #3a3a3a !important; |
||||||
|
color: #ffe082 !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
[class*="bg-"] { |
||||||
|
background-color: inherit !important; |
||||||
|
} |
||||||
|
/* Forcer le texte noir sur fond blanc pour le modal */ |
||||||
|
#processingModal .modal-content, #processingModal .modal-header, #processingModal .modal-body, #processingModal p { |
||||||
|
background-color: #fff !important; |
||||||
|
color: #212529 !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<body class="bg-light"> |
||||||
|
<nav class="navbar navbar-expand navbar-dark bg-dark"> |
||||||
|
<a class="navbar-brand ps-3" href="#">IAProf</a> |
||||||
|
<ul class="navbar-nav ms-auto me-3"> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="/logout/">Se déconnecter</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
<div id="layoutSidenav"> |
||||||
|
<div id="layoutSidenav_content"> |
||||||
|
<main> |
||||||
|
<div class="container-fluid px-4 mt-4"> |
||||||
|
<div class="card shadow mb-4"> |
||||||
|
<div class="card-header py-3"> |
||||||
|
<h2 class="m-0 font-weight-bold text-primary">Choix du fichier de bulletins à traiter :</h2> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
{% if success %} |
||||||
|
<div class="alert alert-success">Fichiers reçus avec succès !<br> |
||||||
|
Appréciations : {{ appreciations_filename }}<br> |
||||||
|
{% if modele_filename %}Modèle : {{ modele_filename }}<br>{% endif %} |
||||||
|
<strong>Résultat du traitement :</strong> {{ resultat }} |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
<div id="processingModal" class="modal fade" tabindex="-1" aria-labelledby="processingModalLabel" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="processingModalLabel">Traitement en cours</h5> |
||||||
|
</div> |
||||||
|
<div class="modal-body text-center"> |
||||||
|
<div class="spinner-border text-primary" role="status"> |
||||||
|
<span class="visually-hidden">Chargement...</span> |
||||||
|
</div> |
||||||
|
<p class="mt-3">Veuillez patienter pendant l'extraction des données du PDF...</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<form method="post" enctype="multipart/form-data" id="generationForm"> |
||||||
|
{% csrf_token %} |
||||||
|
<div class="mb-3"> |
||||||
|
<label for="appreciations_pdf" class="form-label">Fichier PDF du bulletin de la classe à traiter (export Pronote) :</label> |
||||||
|
<input class="form-control" type="file" id="appreciations_pdf" name="appreciations_pdf" |
||||||
|
accept="application/pdf" required> |
||||||
|
</div> |
||||||
|
<button type="submit" class="btn btn-primary">Traiter</button> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> |
||||||
|
<script> |
||||||
|
document.getElementById('generationForm').addEventListener('submit', function(e) { |
||||||
|
var modal = new bootstrap.Modal(document.getElementById('processingModal')); |
||||||
|
modal.show(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
||||||
@ -0,0 +1,140 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="fr"> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||||
|
<title>Connexion - IAProf</title> |
||||||
|
<!-- SB Admin Bootstrap CSS --> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/startbootstrap-sb-admin@7.0.6/dist/css/styles.min.css" rel="stylesheet"> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||||
|
<style> |
||||||
|
:root { |
||||||
|
color-scheme: light dark; |
||||||
|
} |
||||||
|
|
||||||
|
body, |
||||||
|
.bg-light { |
||||||
|
background-color: #f8f9fa; |
||||||
|
color: #212529; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) { |
||||||
|
html, |
||||||
|
body, |
||||||
|
.bg-light { |
||||||
|
background-color: #181a1b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
|
||||||
|
.card, |
||||||
|
.card-header, |
||||||
|
.card-body { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
|
||||||
|
.table { |
||||||
|
color: #f8f9fa; |
||||||
|
background-color: #23272b; |
||||||
|
} |
||||||
|
|
||||||
|
.table-bordered th, |
||||||
|
.table-bordered td { |
||||||
|
border-color: #444c56; |
||||||
|
} |
||||||
|
|
||||||
|
.form-select, |
||||||
|
.form-label, |
||||||
|
.btn, |
||||||
|
.alert { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary { |
||||||
|
background-color: #375a7f !important; |
||||||
|
border-color: #375a7f !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-danger { |
||||||
|
background-color: #c9302c !important; |
||||||
|
border-color: #c9302c !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-success { |
||||||
|
background-color: #449d44 !important; |
||||||
|
border-color: #449d44 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar, |
||||||
|
.navbar-dark, |
||||||
|
.bg-dark { |
||||||
|
background-color: #23272b !important; |
||||||
|
} |
||||||
|
|
||||||
|
.alert-warning { |
||||||
|
background-color: #3a3a3a !important; |
||||||
|
color: #ffe082 !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.form-control { |
||||||
|
background-color: #fff !important; |
||||||
|
color: #212529 !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.form-control::placeholder, |
||||||
|
.form-floating>label { |
||||||
|
color: #212529 !important; |
||||||
|
} |
||||||
|
|
||||||
|
[class*="bg-"] { |
||||||
|
background-color: inherit !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<body class="bg-primary"> |
||||||
|
<div id="layoutAuthentication"> |
||||||
|
<div id="layoutAuthentication_content"> |
||||||
|
<main> |
||||||
|
<div class="container"> |
||||||
|
<div class="row justify-content-center"> |
||||||
|
<div class="col-lg-5"> |
||||||
|
<div class="card shadow-lg border-0 rounded-lg mt-5"> |
||||||
|
<div class="card-header"> |
||||||
|
<h3 class="text-center font-weight-light my-4">Connexion</h3> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<form method="post"> |
||||||
|
{% csrf_token %} |
||||||
|
<div class="form-floating mb-3"> |
||||||
|
<input class="form-control" id="username" name="username" type="text" |
||||||
|
placeholder="Nom d'utilisateur" required /> |
||||||
|
<label for="username">Nom d'utilisateur</label> |
||||||
|
</div> |
||||||
|
<div class="form-floating mb-3"> |
||||||
|
<input class="form-control" id="password" name="password" type="password" |
||||||
|
placeholder="Mot de passe" required /> |
||||||
|
<label for="password">Mot de passe</label> |
||||||
|
</div> |
||||||
|
<div class="d-flex align-items-center justify-content-between mt-4 mb-0"> |
||||||
|
<button class="btn btn-primary w-100" type="submit">Se connecter</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> |
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
||||||
@ -0,0 +1,199 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="fr"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||||
|
<title>IAProf</title> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/startbootstrap-sb-admin@7.0.6/dist/css/styles.min.css" rel="stylesheet"> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||||
|
<style> |
||||||
|
:root { |
||||||
|
color-scheme: light dark; |
||||||
|
} |
||||||
|
body, .bg-light { |
||||||
|
background-color: #f8f9fa; |
||||||
|
color: #212529; |
||||||
|
} |
||||||
|
@media (prefers-color-scheme: dark) { |
||||||
|
html, body, .bg-light { |
||||||
|
background-color: #181a1b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
.card, .card-header, .card-body { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
.table, .table-bordered { |
||||||
|
color: #f8f9fa; |
||||||
|
background-color: #23272b !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
.table-bordered th, .table-bordered td { |
||||||
|
border-color: #444c56 !important; |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
} |
||||||
|
.table-striped > tbody > tr:nth-of-type(odd) { |
||||||
|
background-color: #20232a !important; |
||||||
|
} |
||||||
|
.form-select, .form-label, .btn, .alert { |
||||||
|
background-color: #23272b !important; |
||||||
|
color: #f8f9fa !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
.btn-primary { |
||||||
|
background-color: #375a7f !important; |
||||||
|
border-color: #375a7f !important; |
||||||
|
} |
||||||
|
.btn-danger { |
||||||
|
background-color: #c9302c !important; |
||||||
|
border-color: #c9302c !important; |
||||||
|
} |
||||||
|
.btn-success { |
||||||
|
background-color: #449d44 !important; |
||||||
|
border-color: #449d44 !important; |
||||||
|
} |
||||||
|
.navbar, .navbar-dark, .bg-dark { |
||||||
|
background-color: #23272b !important; |
||||||
|
} |
||||||
|
.alert-warning { |
||||||
|
background-color: #3a3a3a !important; |
||||||
|
color: #ffe082 !important; |
||||||
|
border-color: #444c56 !important; |
||||||
|
} |
||||||
|
[class*="bg-"] { |
||||||
|
background-color: inherit !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="bg-light"> |
||||||
|
<nav class="navbar navbar-expand navbar-dark bg-dark"> |
||||||
|
<a class="navbar-brand ps-3" href="#">IAProf</a> |
||||||
|
<ul class="navbar-nav ms-auto me-3"> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="/logout/">Se déconnecter</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
<div id="layoutSidenav"> |
||||||
|
<div id="layoutSidenav_content"> |
||||||
|
<main> |
||||||
|
<div class="container-fluid px-4 mt-4"> |
||||||
|
<div class="card shadow mb-4"> |
||||||
|
<div class="card-header py-3"> |
||||||
|
<h2 class="m-0 font-weight-bold text-primary">Génération des appréciations</h2> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="mb-3"> |
||||||
|
<label for="modele-select" class="form-label">Choisir un modèle</label> |
||||||
|
<select id="modele-select" class="form-select"> |
||||||
|
<option value="ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON">Modèle Gangneux</option> |
||||||
|
<option value="gpt-4.1-mini">GPT-4.1 mini</option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<button id="generation-toggle" class="btn btn-primary mb-3">Lancer la génération</button> |
||||||
|
<table class="table table-bordered" id="appreciations-table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Élève</th> |
||||||
|
<th>Appréciation</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% if appreciations_json and appreciations_json|length > 0 %} |
||||||
|
{% for appreciation in appreciations_json %} |
||||||
|
<tr> |
||||||
|
<td class="eleve">{{ appreciation.eleve }}</td> |
||||||
|
<td class="appreciation"></td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
{% else %} |
||||||
|
<tr> |
||||||
|
<td colspan="2"><div class="alert alert-warning mb-0">Aucune appréciation générée.</div></td> |
||||||
|
</tr> |
||||||
|
{% endif %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<script> |
||||||
|
let stopGeneration = false; |
||||||
|
let enCours = false; |
||||||
|
|
||||||
|
function getRowsSansAppreciation() { |
||||||
|
return Array.from(document.querySelectorAll('#appreciations-table tbody tr')).filter(row => !row.querySelector('.appreciation').textContent.trim()); |
||||||
|
} |
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() { |
||||||
|
const btnToggle = document.getElementById('generation-toggle'); |
||||||
|
const selectModele = document.getElementById('modele-select'); |
||||||
|
let rows = getRowsSansAppreciation(); |
||||||
|
|
||||||
|
function setButtonState(state) { |
||||||
|
if (state === 'lancer') { |
||||||
|
btnToggle.textContent = 'Lancer la génération'; |
||||||
|
btnToggle.className = 'btn btn-primary mb-3'; |
||||||
|
} else if (state === 'arreter') { |
||||||
|
btnToggle.textContent = 'Arrêter la génération'; |
||||||
|
btnToggle.className = 'btn btn-danger mb-3'; |
||||||
|
} else if (state === 'continuer') { |
||||||
|
btnToggle.textContent = 'Continuer la génération'; |
||||||
|
btnToggle.className = 'btn btn-success mb-3'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function traiterLignes() { |
||||||
|
rows = getRowsSansAppreciation(); |
||||||
|
if (rows.length === 0 || stopGeneration) { |
||||||
|
enCours = false; |
||||||
|
if (stopGeneration) setButtonState('continuer'); |
||||||
|
else setButtonState('lancer'); |
||||||
|
return; |
||||||
|
} |
||||||
|
enCours = true; |
||||||
|
setButtonState('arreter'); |
||||||
|
const row = rows[0]; // Toujours traiter la première ligne sans appréciation |
||||||
|
const eleve = row.querySelector('.eleve').textContent; |
||||||
|
const appreciationCell = row.querySelector('.appreciation'); |
||||||
|
appreciationCell.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Génération...'; |
||||||
|
fetch("{% url 'generer_appreciation_ajax' %}", { |
||||||
|
method: 'POST', |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/json', |
||||||
|
'X-CSRFToken': '{{ csrf_token }}' |
||||||
|
}, |
||||||
|
body: JSON.stringify({eleve: eleve, modele: selectModele.value}) |
||||||
|
}) |
||||||
|
.then(response => response.json()) |
||||||
|
.then(data => { |
||||||
|
appreciationCell.textContent = data.appreciation; |
||||||
|
traiterLignes(); |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
appreciationCell.textContent = 'Erreur lors de la génération'; |
||||||
|
traiterLignes(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
btnToggle.addEventListener('click', function() { |
||||||
|
if (!enCours && (btnToggle.textContent === 'Lancer la génération' || btnToggle.textContent === 'Continuer la génération')) { |
||||||
|
stopGeneration = false; |
||||||
|
setButtonState('arreter'); |
||||||
|
traiterLignes(); |
||||||
|
} else if (enCours && btnToggle.textContent === 'Arrêter la génération') { |
||||||
|
stopGeneration = true; |
||||||
|
setButtonState('continuer'); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setButtonState('lancer'); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
from django.test import TestCase |
||||||
|
from django.urls import reverse |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
class LoginViewTests(TestCase): |
||||||
|
def setUp(self): |
||||||
|
self.username = 'testuser' |
||||||
|
self.password = 'testpassword' |
||||||
|
self.user = User.objects.create_user(username=self.username, password=self.password) |
||||||
|
|
||||||
|
def test_login_view_redirects_authenticated_user(self): |
||||||
|
self.client.login(username=self.username, password=self.password) |
||||||
|
response = self.client.get(reverse('hello_world')) |
||||||
|
self.assertEqual(response.status_code, 200) |
||||||
|
|
||||||
|
def test_login_view_renders_login_template_for_anonymous_user(self): |
||||||
|
response = self.client.get(reverse('login')) |
||||||
|
self.assertEqual(response.status_code, 200) |
||||||
|
self.assertTemplateUsed(response, 'login.html') |
||||||
|
|
||||||
|
def test_logout_redirects_to_login(self): |
||||||
|
self.client.login(username=self.username, password=self.password) |
||||||
|
response = self.client.post(reverse('logout')) |
||||||
|
self.assertRedirects(response, reverse('login')) |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
from django.urls import path |
||||||
|
from .views import login_view, generation, resultat_appreciations, logout_view, generer_appreciation_ajax |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('login/', login_view, name='login'), |
||||||
|
path('generation/', generation, name='generation'), |
||||||
|
path('resultat/', resultat_appreciations, name='resultat_appreciations'), |
||||||
|
path('logout/', logout_view, name='logout'), |
||||||
|
path('generer_appreciation_ajax/', generer_appreciation_ajax, name='generer_appreciation_ajax'), |
||||||
|
] |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
from django.shortcuts import render, redirect |
||||||
|
from django.contrib.auth import authenticate, login, logout |
||||||
|
from django.contrib.auth.decorators import login_required |
||||||
|
from django.conf import settings |
||||||
|
from main.openAIAppreciations import * |
||||||
|
from django.urls import reverse |
||||||
|
from django.http import JsonResponse |
||||||
|
import os |
||||||
|
import uuid |
||||||
|
import json |
||||||
|
|
||||||
|
def login_view(request): |
||||||
|
if request.method == 'POST': |
||||||
|
username = request.POST['username'] |
||||||
|
password = request.POST['password'] |
||||||
|
user = authenticate(request, username=username, password=password) |
||||||
|
if user is not None: |
||||||
|
login(request, user) |
||||||
|
return redirect('generation') |
||||||
|
return render(request, 'login.html') |
||||||
|
|
||||||
|
@login_required(login_url='/login/') |
||||||
|
def generation(request): |
||||||
|
if request.method == 'POST': |
||||||
|
appreciations_pdf = request.FILES.get('appreciations_pdf') |
||||||
|
appreciations_path = None |
||||||
|
tmp_dir = os.path.join(settings.BASE_DIR, 'main', 'tmp') |
||||||
|
os.makedirs(tmp_dir, exist_ok=True) |
||||||
|
if appreciations_pdf: |
||||||
|
ext = os.path.splitext(appreciations_pdf.name)[1] |
||||||
|
random_name = f"appreciations_{uuid.uuid4().hex}{ext}" |
||||||
|
appreciations_path = os.path.join(tmp_dir, random_name) |
||||||
|
with open(appreciations_path, 'wb+') as destination: |
||||||
|
for chunk in appreciations_pdf.chunks(): |
||||||
|
destination.write(chunk) |
||||||
|
# Appel du traitement après upload |
||||||
|
appreciations_json = convertPdfToJSON(appreciations_path) |
||||||
|
if appreciations_path and os.path.exists(appreciations_path): |
||||||
|
os.remove(appreciations_path) |
||||||
|
request.session['appreciations_json'] = appreciations_json |
||||||
|
return redirect('resultat_appreciations') |
||||||
|
return render(request, 'generation.html') |
||||||
|
|
||||||
|
@login_required(login_url='/login/') |
||||||
|
def resultat_appreciations(request): |
||||||
|
appreciations_result = request.session.get('appreciations_json', []) |
||||||
|
return render(request, 'resultat_appreciations.html', {'appreciations_json': appreciations_result}) |
||||||
|
|
||||||
|
@login_required(login_url='/login/') |
||||||
|
def generer_appreciation_ajax(request): |
||||||
|
if request.method == 'POST': |
||||||
|
data = json.loads(request.body) |
||||||
|
eleve = data.get('eleve') |
||||||
|
modele = data.get('modele') |
||||||
|
appreciation = generer_appreciation_pour_eleve(eleve, request.session.get('appreciations_json', []), modele) |
||||||
|
return JsonResponse({'appreciation': appreciation}) |
||||||
|
return JsonResponse({'error': 'Méthode non autorisée'}, status=405) |
||||||
|
|
||||||
|
def logout_view(request): |
||||||
|
logout(request) |
||||||
|
return redirect('login') |
||||||
|
|
||||||
|
def generer_appreciation_pour_eleve(eleve, eleves_json, modele=None): |
||||||
|
eleve_data = next((e for e in eleves_json if e["eleve"] == eleve), None) |
||||||
|
if not eleve_data: |
||||||
|
return f"Aucun élève trouvé avec le nom {eleve}" |
||||||
|
if modele is None: |
||||||
|
modele = "ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON" |
||||||
|
completion = openai.ChatCompletion.create( |
||||||
|
model=modele, |
||||||
|
messages=[ |
||||||
|
{"role": "system", "content": "Rédige une appréciation générale (500 caractères max) pour cet élève"}, |
||||||
|
{"role": "user", "content": str(eleve_data["appreciations"])} |
||||||
|
], |
||||||
|
temperature=0.7, |
||||||
|
presence_penalty=0.6, |
||||||
|
frequency_penalty=0.6, |
||||||
|
top_p=0.5) |
||||||
|
return completion.choices[0].message.content |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
import os |
||||||
|
import sys |
||||||
|
|
||||||
|
def main(): |
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ia_prof.settings') |
||||||
|
try: |
||||||
|
from django.core.management import execute_from_command_line |
||||||
|
except ImportError as exc: |
||||||
|
raise ImportError( |
||||||
|
"Couldn't import Django. Are you sure it's installed and " |
||||||
|
"available on your PYTHONPATH environment variable? Did you " |
||||||
|
"forget to activate a virtual environment?" |
||||||
|
) from exc |
||||||
|
execute_from_command_line(sys.argv) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
||||||
Loading…
Reference in new issue