Compare commits

...

No commits in common. 'master' and 'main' have entirely different histories.
master ... main

  1. 156
      .gitignore
  2. 36
      Dockerfile
  3. 97
      README.md
  4. 6
      default.env
  5. 1
      ia_prof/__init__.py
  6. 13
      ia_prof/asgi.py
  7. 115
      ia_prof/settings.py
  8. 9
      ia_prof/urls.py
  9. 15
      ia_prof/wsgi.py
  10. 1
      main/__init__.py
  11. 18
      main/admin.py
  12. 5
      main/apps.py
  13. 17
      main/consumers.py
  14. 24
      main/models.py
  15. 72
      main/openAIAppreciations.py
  16. 218
      main/templates/generation.html
  17. 140
      main/templates/login.html
  18. 446
      main/templates/resultat_appreciations.html
  19. 26
      main/tests.py
  20. 11
      main/urls.py
  21. 119
      main/views.py
  22. 18
      manage.py
  23. 7
      requirements.txt

156
.gitignore vendored

@ -1,154 +1,6 @@
# ---> Python venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal __pycache__
migrations
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env .env
.venv .vscode
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

36
Dockerfile

@ -0,0 +1,36 @@
# Utilise une image Python officielle
FROM python:3.12-slim
# Variables d'environnement
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Installer les dépendances système
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
apache2 \
apache2-dev \
&& rm -rf /var/lib/apt/lists/*
# Installer mod_wsgi
RUN pip install mod_wsgi
# Créer le dossier de l'application
WORKDIR /code
# Copier les fichiers de l'application
COPY . /code/
# Installer les dépendances Python
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# Collecte des fichiers statiques
RUN python manage.py collectstatic --noinput
# Exposer le port 8000
EXPOSE 8000
# Commande de lancement avec mod_wsgi-express (utilisation d'un utilisateur non-root)
CMD mod_wsgi-express start-server --user www-data --group www-data --port 8000 --url-alias /static /code/static /code/ia_prof/wsgi.py

97
README.md

@ -1,2 +1,99 @@
# IAProf # 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 makemigrations main
python manage.py migrate
```
6. Lancez le serveur de développement :
```
python manage.py runserver
```
7. Créez un fichier `.env` à la racine du projet (ou copiez `default.env` puis renommez-le en `.env`) et renseignez vos variables sensibles :
```
DJANGO_SECRET_KEY=<votre_clé_secrète>
OPENAI_API_KEY=<votre_clé_openai>
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
```
8. (Optionnel mais recommandé) Créez un superutilisateur pour accéder à l’interface d’administration Django :
```
python manage.py createsuperuser
```
Suivez les instructions pour définir un nom d’utilisateur, une adresse e-mail et un mot de passe.
## 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 de connexion.
- Pour accéder à l’interface d’administration Django, allez sur `http://127.0.0.1:8000/admin` et connectez-vous avec le compte superutilisateur créé.

6
default.env

@ -0,0 +1,6 @@
# Fichier .env pour Django (à placer à la racine du projet)
DJANGO_SECRET_KEY=
OPENAI_API_KEY=
DJANGO_DEBUG=1
DJANGO_ALLOWED_HOSTS=*

1
ia_prof/__init__.py

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

13
ia_prof/asgi.py

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

115
ia_prof/settings.py

@ -0,0 +1,115 @@
import os
from pathlib import Path
# Charger les variables d'environnement depuis un fichier .env si présent
from dotenv import load_dotenv
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '')
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '')
DEBUG = os.environ.get('DJANGO_DEBUG', '') == '1'
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') if not DEBUG else []
CSRF_TRUSTED_ORIGINS = os.environ.get('DJANGO_CSRF_TRUSTED_ORIGINS', '').split(',') if not DEBUG else []
# 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/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'main', 'templates', 'static')] if os.path.exists(os.path.join(BASE_DIR, 'main', 'templates', 'static')) else []
# Default primary key field type
# https://docs.djangoproject.com/en/4.x/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

9
ia_prof/urls.py

@ -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 @@
"""
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 @@
# This file is intentionally left blank.

18
main/admin.py

@ -0,0 +1,18 @@
from django.contrib import admin
from .models import UserProfile, Modele, UserCredit
@admin.register(Modele)
class ModeleAdmin(admin.ModelAdmin):
list_display = ('nom', 'code', 'actif')
list_editable = ('code', 'actif')
search_fields = ('nom', 'code')
list_filter = ('actif',)
@admin.register(UserCredit)
class UserCreditAdmin(admin.ModelAdmin):
list_display = ('user', 'credit')
search_fields = ('user__username',)
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('user',)

5
main/apps.py

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

24
main/models.py

@ -0,0 +1,24 @@
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
class Modele(models.Model):
nom = models.CharField(max_length=255)
code = models.CharField(max_length=255, unique=True, default="gpt-4.1-mini")
actif = models.BooleanField(default=True)
def __str__(self):
return self.nom
class UserCredit(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='credit')
credit = models.IntegerField(default=10)
def __str__(self):
return f"{self.user.username} - Crédit: {self.credit}"

72
main/openAIAppreciations.py

@ -0,0 +1,72 @@
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

218
main/templates/generation.html

@ -0,0 +1,218 @@
<!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;
}
#processingModal .modal-content, #processingModal .modal-header, #processingModal .modal-body, #processingModal p, #processingModal .modal-body * {
background-color: #fff !important;
color: #212529 !important;
}
@media (prefers-color-scheme: dark) {
#processingModal .modal-content, #processingModal .modal-header, #processingModal .modal-body, #processingModal p, #processingModal .modal-body * {
background-color: #fff !important;
color: #212529 !important;
}
}
#btn-aide {
background-color: #ffe082 !important;
color: #23272b !important;
border: 1px solid #444c56 !important;
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
#btn-aide {
background-color: #ffe082 !important;
color: #23272b !important;
border: 1px solid #444c56 !important;
font-weight: 600;
}
}
}
#aideModal .modal-content, #aideModal .modal-header, #aideModal .modal-body, #aideModal p, #aideModal .modal-body * {
background-color: #fff !important;
color: #212529 !important;
}
@media (prefers-color-scheme: dark) {
#aideModal .modal-content, #aideModal .modal-header, #aideModal .modal-body, #aideModal p, #aideModal .modal-body * {
background-color: #fff !important;
color: #212529 !important;
}
}
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
min-height: 100vh;
overflow: hidden;
}
#layoutSidenav {
min-height: 0;
height: 100%;
overflow: hidden;
}
#layoutSidenav_content {
min-height: 0;
height: 100%;
overflow: hidden;
}
main {
min-height: 0;
height: 100%;
overflow: hidden;
}
.container-fluid {
min-height: 0;
overflow: hidden;
}
.card.shadow.mb-4 {
min-height: 0;
overflow: hidden;
}
.card-body {
min-height: 0;
overflow: hidden;
}
</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 d-flex justify-content-between align-items-center">
<h2 class="m-0 font-weight-bold text-primary">Choix du fichier de bulletins à traiter :</h2>
<button id="btn-aide" type="button" class="btn btn-info btn-sm" data-bs-toggle="modal" data-bs-target="#aideModal">Aide</button>
</div>
<div class="modal fade" id="aideModal" tabindex="-1" aria-labelledby="aideModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="aideModalLabel">Aide : Exporter un PDF Pronote</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
Dans Pronote, aller dans <b>Bulletins &gt; Bulletin</b>, choisir la classe et appuyer en haut à droite sur <b>PDF</b>, puis sur l'engrenage en sélectionnant <b>"Bulletins élèves de toute la classe"</b>.<br>Valider puis appuyer sur <b>Voir le PDF</b>.<br>Enregistrer ce PDF pour le mettre dans l'application IAProf.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</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 @@
<!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>

446
main/templates/resultat_appreciations.html

@ -0,0 +1,446 @@
<!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;
}
#credit-restant {
background-color: #ffe082 !important;
color: #23272b !important;
border: 1px solid #444c56 !important;
}
.btn-outline-primary, .btn-outline-primary:focus, .btn-outline-primary:active, .btn-outline-primary:hover {
color: #23272b !important;
background-color: #ffe082 !important;
border-color: #ffe082 !important;
box-shadow: none !important;
}
.btn-outline-primary:disabled, .btn-outline-primary.disabled {
color: #bdbdbd !important;
background-color: #444c56 !important;
border-color: #444c56 !important;
}
}
#bulletinModal .modal-content, #bulletinModal .modal-body, #bulletinModal .modal-header {
background-color: #fff !important;
color: #23272b !important;
}
#bulletinModal .modal-title {
color: #23272b !important;
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<a class="navbar-brand ps-3" href="/generation/">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 d-flex justify-content-between align-items-center">
<div>
<label for="modele-select" class="form-label">Choisir un modèle</label>
<select id="modele-select" class="form-select">
{% for modele in modeles|dictsortreversed:'id' %}
<option value="{{ modele.code }}" {% if modele.code == 'gpt-4.1-mini' %}selected{% endif %}>{{ modele.nom }}</option>
{% endfor %}
{% if not modeles or not modeles|dictsort:'code'|length %}
<option value="gpt-4.1-mini" selected>gpt-4.1-mini</option>
{% endif %}
</select>
</div>
<div>
<span class="badge bg-info text-dark" id="credit-restant">Crédits restants : {{ credit }}</span>
</div>
</div>
<div class="mb-3 d-flex gap-2">
<button id="generation-toggle" class="btn btn-primary mb-3">Lancer la génération</button>
<button id="export-pdf" class="btn btn-primary mb-3" type="button" onclick="exportTableToPDF()">Exporter en PDF</button>
</div>
<div id="credit-alert" class="alert alert-danger d-none" role="alert">
Vous n'avez plus de crédits pour générer des appréciations.
</div>
<table class="table table-bordered" id="appreciations-table">
<thead>
<tr>
<th style="width:1%; white-space:nowrap;">Élève</th>
<th>Appréciation</th>
<th style="width:1%; white-space:nowrap; text-align:center;">Action</th>
</tr>
</thead>
<tbody>
{% if appreciations_json and appreciations_json|length > 0 %}
{% for appreciation in appreciations_json %}
<tr>
<td class="eleve" style="width:1%; white-space:nowrap;">{{ appreciation.eleve }}</td>
<td class="appreciation"></td>
<td style="width:1%; text-align:center; vertical-align:middle;">
<div class="d-flex justify-content-center gap-2">
<button class="btn btn-outline-primary btn-sm action-generate w-100" type="button">Générer</button>
<button class="btn btn-outline-primary btn-sm action-bulletin w-100" type="button" data-eleve="{{ appreciation.eleve }}">Bulletin</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3"><div class="alert alert-warning mb-0">Aucune appréciation générée.</div></td>
</tr>
{% endif %}
</tbody>
</table>
<!-- Injection des appréciations dans une variable JS globale pour accès rapide -->
<script id="appreciations-data" type="application/json">
{{ appreciations_json|safe }}
</script>
<script>
window.allAppreciations = {};
try {
const appreciationsList = JSON.parse(document.getElementById('appreciations-data').textContent);
appreciationsList.forEach(eleve => {
let appreciations = eleve.appreciations;
if (typeof appreciations === 'string') {
try { appreciations = JSON.parse(appreciations); } catch (e) { appreciations = []; }
}
window.allAppreciations[eleve.eleve] = appreciations;
});
} catch (e) { window.allAppreciations = {}; }
</script>
<script>
let stopGeneration = false;
let enCours = false;
function updateActionButtons() {
const rows = document.querySelectorAll('#appreciations-table tbody tr');
rows.forEach(row => {
const appreciationCell = row.querySelector('.appreciation');
const btn = row.querySelector('.action-generate');
if (!btn) return;
if (appreciationCell.textContent.trim()) {
btn.textContent = 'Régénérer';
} else {
btn.textContent = 'Générer';
}
btn.disabled = enCours;
});
}
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');
const creditRestant = document.getElementById('credit-restant');
const creditAlert = document.getElementById('credit-alert');
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();
updateActionButtons();
if (rows.length === 0 || stopGeneration) {
enCours = false;
updateActionButtons();
if (stopGeneration) setButtonState('continuer');
else setButtonState('lancer');
return;
}
enCours = true;
updateActionButtons();
setButtonState('arreter');
const row = rows[0];
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;
updateActionButtons();
if (typeof data.credit !== 'undefined') {
creditRestant.textContent = 'Crédits restants : ' + data.credit;
}
if (data.stop) {
stopGeneration = true;
setButtonState('lancer');
if (data.credit === 0) {
creditAlert.classList.remove('d-none');
}
return;
}
traiterLignes();
})
.catch(() => {
appreciationCell.textContent = 'Erreur lors de la génération';
updateActionButtons();
traiterLignes();
});
}
btnToggle.addEventListener('click', function() {
const credit = parseInt(creditRestant.textContent.replace(/\D/g, ''));
if (credit === 0) {
creditAlert.classList.remove('d-none');
return;
} else {
creditAlert.classList.add('d-none');
}
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');
}
});
// Action individuelle sur bouton Générer/Régénérer
document.querySelectorAll('.action-generate').forEach(btn => {
btn.addEventListener('click', function() {
if (enCours) return;
const row = btn.closest('tr');
const eleve = row.querySelector('.eleve').textContent;
const appreciationCell = row.querySelector('.appreciation');
appreciationCell.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Génération...';
btn.disabled = true;
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;
updateActionButtons();
if (typeof data.credit !== 'undefined') {
creditRestant.textContent = 'Crédits restants : ' + data.credit;
}
if (data.credit === 0) {
creditAlert.classList.remove('d-none');
}
})
.catch(() => {
appreciationCell.textContent = 'Erreur lors de la génération';
updateActionButtons();
});
});
});
// Action individuelle sur bouton Bulletin
document.querySelectorAll('.action-bulletin').forEach(btn => {
btn.addEventListener('click', function() {
const eleve = btn.getAttribute('data-eleve');
fetch("{% url 'get_bulletin_eleve' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({eleve: eleve})
})
.then(response => response.json())
.then(data => {
// Mettre le titre dans le header du modal
const modalTitle = document.getElementById('bulletinModalLabel');
if (modalTitle) {
modalTitle.textContent = `Bulletin de l'élève ${eleve}`;
}
// Mettre uniquement les appréciations dans le body
let message = '';
if (Array.isArray(data.appreciations) && data.appreciations.length > 0) {
message += data.appreciations.map(app => `<div style='margin-bottom:8px;'><strong>${app.matiere} :</strong><br>${app.appreciation}</div>`).join('');
} else {
message += '<em>Aucune appréciation trouvée dans le bulletin.</em>';
}
showBulletinModal(message);
})
.catch(() => {
const modalTitle = document.getElementById('bulletinModalLabel');
if (modalTitle) {
modalTitle.textContent = `Bulletin`;
}
showBulletinModal('<em>Erreur lors de la récupération du bulletin.</em>');
});
});
});
setButtonState('lancer');
updateActionButtons();
});
</script>
<script>
// CDN jsPDF + autoTable pour export PDF
document.addEventListener('DOMContentLoaded', function() {
if (!window.jspdfLoaded) {
const script1 = document.createElement('script');
script1.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
script1.onload = function() {
const script2 = document.createElement('script');
script2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js';
script2.onload = function() { window.jspdfLoaded = true; };
document.body.appendChild(script2);
};
document.body.appendChild(script1);
}
});
function exportTableToPDF() {
const doc = new window.jspdf.jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
doc.text('Tableau des appréciations', 40, 40);
// Couleurs fixes mode clair
const headColor = [55, 90, 127];
const textColor = [33, 37, 41];
const bgColor = [255, 255, 255];
const borderColor = [200, 200, 200];
const table = document.getElementById('appreciations-table');
const rows = Array.from(table.querySelectorAll('tbody tr'))
.map(tr => [
tr.querySelector('.eleve')?.textContent.trim() || '',
tr.querySelector('.appreciation')?.textContent.trim() || ''
]);
doc.autoTable({
head: [['Élève', 'Appréciation']],
body: rows,
startY: 60,
margin: { left: 30, right: 30 },
styles: { fontSize: 11, overflow: 'linebreak', cellWidth: 'auto', textColor: textColor, fillColor: bgColor, lineColor: borderColor },
headStyles: { fillColor: headColor, textColor: textColor, lineColor: borderColor },
bodyStyles: { fillColor: bgColor, textColor: textColor, lineColor: borderColor },
alternateRowStyles: { fillColor: [245, 245, 245] },
columnStyles: {
0: { cellWidth: 'auto'},
1: { cellWidth: 'auto' }
},
tableWidth: 'auto',
pageBreak: 'auto',
});
doc.save('appreciations.pdf');
}
</script>
<script>
function showBulletinModal(html) {
const modalBody = document.getElementById('bulletinModalBody');
if (modalBody) {
modalBody.innerHTML = html;
const modal = new bootstrap.Modal(document.getElementById('bulletinModal'));
modal.show();
}
}
</script>
<!-- Modal Bulletin -->
<div class="modal fade" id="bulletinModal" tabindex="-1" aria-labelledby="bulletinModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:80vw; width:80vw;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bulletinModalLabel">Bulletin</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body" id="bulletinModalBody" style="font-size:1.1rem; line-height:1.5; word-break:break-word;"></div>
</div>
</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>

26
main/tests.py

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

11
main/urls.py

@ -0,0 +1,11 @@
from django.urls import path
from .views import login_view, generation, resultat_appreciations, logout_view, generer_appreciation_ajax, get_bulletin_eleve
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'),
path('get_bulletin_eleve/', get_bulletin_eleve, name='get_bulletin_eleve'),
]

119
main/views.py

@ -0,0 +1,119 @@
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
from django.contrib.auth.models import User
from main.models import UserCredit, Modele
import os
import uuid
import json
from django.views.decorators.csrf import csrf_exempt
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', [])
credit = 0
if hasattr(request.user, 'credit'):
credit = request.user.credit.credit
modeles = Modele.objects.filter(actif=True)
return render(request, 'resultat_appreciations.html', {
'appreciations_json': appreciations_result,
'credit': credit,
'modeles': modeles
})
@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')
user = request.user
# Vérifier le crédit
if hasattr(user, 'credit') and user.credit.credit > 0:
appreciation = generer_appreciation_pour_eleve(eleve, request.session.get('appreciations_json', []), modele)
# Décrémenter le crédit
user.credit.credit -= 1
user.credit.save()
credit = user.credit.credit
stop = credit == 0
return JsonResponse({'appreciation': appreciation, 'credit': credit, 'stop': stop})
else:
return JsonResponse({'appreciation': 'Crédit épuisé', 'credit': 0, 'stop': True})
return JsonResponse({'error': 'Méthode non autorisée'}, status=405)
@login_required(login_url='/login/')
def get_bulletin_eleve(request):
if request.method == 'POST':
data = json.loads(request.body)
eleve_nom = data.get('eleve')
appreciations_json = request.session.get('appreciations_json', [])
appreciations = []
for eleve in appreciations_json:
if eleve.get('eleve') == eleve_nom:
appreciations = eleve.get('appreciations', [])
if isinstance(appreciations, str):
try:
appreciations = json.loads(appreciations)
except Exception:
appreciations = []
break
return JsonResponse({'appreciations': appreciations})
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.replace("\\n", " ").strip()

18
manage.py

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

7
requirements.txt

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