Browse Source

Version 1.0.0

main
scayac 6 months ago
commit
e1a68eb65c
  1. 4
      .gitignore
  2. 93
      README.md
  3. 1
      ia_prof/__init__.py
  4. 13
      ia_prof/asgi.py
  5. 116
      ia_prof/settings.py
  6. 9
      ia_prof/urls.py
  7. 15
      ia_prof/wsgi.py
  8. 1
      main/__init__.py
  9. 4
      main/admin.py
  10. 5
      main/apps.py
  11. 17
      main/consumers.py
  12. 9
      main/models.py
  13. 90
      main/openAIAppreciations.py
  14. 129
      main/openaiApprecioations copy.py
  15. 136
      main/templates/generation.html
  16. 140
      main/templates/login.html
  17. 199
      main/templates/resultat_appreciations.html
  18. 26
      main/tests.py
  19. 10
      main/urls.py
  20. 79
      main/views.py
  21. 18
      manage.py
  22. 6
      requirements.txt

4
.gitignore vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
venv
db.sqlite3
__pycache__
migrations

93
README.md

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
# IAProf
IAProf est une application web Django intégrant le module Python d’OpenAI et utilisant les WebSockets pour la communication en temps réel. Ce projet propose un système de connexion simple qui redirige l’utilisateur vers une vue « Hello World » après authentification.
## Fonctionnalités
- **Framework Django** : Application basée sur Django, un framework web Python rapide et structurant.
- **Intégration OpenAI** : Utilisation du module Python OpenAI pour des fonctionnalités d’IA.
- **Base SQLite** : Base de données légère et facile à configurer.
- **Authentification** : Page de connexion redirigeant vers une vue « Hello World » si l’utilisateur est authentifié.
## Structure du projet
```
IAProf
├── ia_prof
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── main
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── consumers.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ ├── hello_world.html
│ │ └── login.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
├── requirements.txt
├── README.md
└── venv
```
## Installation
1. Clonez le dépôt :
```
git clone <repository-url>
cd IAProf
```
2. Créez un environnement virtuel :
```
python -m venv venv
```
3. Activez l’environnement virtuel :
- Sous Windows :
```
venv\Scripts\activate
```
- Sous macOS/Linux :
```
source venv/bin/activate
```
4. Installez les dépendances :
```
pip install -r requirements.txt
```
5. Appliquez les migrations :
```
python manage.py migrate
```
6. Lancez le serveur de développement :
```
python manage.py runserver
```
## Configuration de la clé OpenAI
La clé API OpenAI doit être définie dans le fichier `ia_prof/settings.py` :
```python
OPENAI_API_KEY = "votre_clé_openai"
```
## Utilisation
- Rendez-vous sur `http://127.0.0.1:8000/login` pour accéder à la page de connexion.
- Après connexion, vous serez redirigé vers la vue « Hello World ».
Dans le code, utilisez-la via `settings.OPENAI_API_KEY` pour sécuriser et centraliser la configuration.

1
ia_prof/__init__.py

@ -0,0 +1 @@ @@ -0,0 +1 @@
# This file is intentionally left blank.

13
ia_prof/asgi.py

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

116
ia_prof/settings.py

@ -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"

9
ia_prof/urls.py

@ -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')),
]

15
ia_prof/wsgi.py

@ -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()

1
main/__init__.py

@ -0,0 +1 @@ @@ -0,0 +1 @@
# This file is intentionally left blank.

4
main/admin.py

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
from django.contrib import admin
from .models import UserProfile
admin.site.register(UserProfile)

5
main/apps.py

@ -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'

17
main/consumers.py

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

9
main/models.py

@ -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

90
main/openAIAppreciations.py

@ -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

129
main/openaiApprecioations copy.py

@ -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

136
main/templates/generation.html

@ -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>

140
main/templates/login.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>

199
main/templates/resultat_appreciations.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>

26
main/tests.py

@ -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'))

10
main/urls.py

@ -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'),
]

79
main/views.py

@ -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

18
manage.py

@ -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()

6
requirements.txt

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
Django==4.2
djangorestframework==3.14.0
openai==0.27.0
PyPDF2
openai
pdfplumber
Loading…
Cancel
Save