commit
e1a68eb65c
22 changed files with 1120 additions and 0 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
venv |
||||
db.sqlite3 |
||||
__pycache__ |
||||
migrations |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
# This file is intentionally left blank. |
||||
@ -0,0 +1,13 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
# This file is intentionally left blank. |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
from django.contrib import admin |
||||
from .models import UserProfile |
||||
|
||||
admin.site.register(UserProfile) |
||||
@ -0,0 +1,5 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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