Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
23 changed files with 1418 additions and 152 deletions
@ -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/ |
|
||||||
|
|
||||||
@ -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 |
||||||
@ -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=* |
||||||
@ -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,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 |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
from django.urls import path, include, re_path |
||||||
|
from django.views.generic.base import RedirectView |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('admin/', admin.site.urls), |
||||||
|
re_path(r'^$', RedirectView.as_view(url='/login/', permanent=False)), |
||||||
|
path('', include('main.urls')), |
||||||
|
] |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
""" |
||||||
|
WSGI config for IAProf project. |
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named `application`. |
||||||
|
|
||||||
|
For more information on this file, see |
||||||
|
https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/ |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
from django.core.wsgi import get_wsgi_application |
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ia_prof.settings') |
||||||
|
|
||||||
|
application = get_wsgi_application() |
||||||
@ -0,0 +1,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',) |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
class MainConfig(AppConfig): |
||||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||||
|
name = 'main' |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer |
||||||
|
import json |
||||||
|
|
||||||
|
class ChatConsumer(AsyncWebsocketConsumer): |
||||||
|
async def connect(self): |
||||||
|
await self.accept() |
||||||
|
|
||||||
|
async def disconnect(self, close_code): |
||||||
|
pass |
||||||
|
|
||||||
|
async def receive(self, text_data): |
||||||
|
text_data_json = json.loads(text_data) |
||||||
|
message = text_data_json['message'] |
||||||
|
|
||||||
|
await self.send(text_data=json.dumps({ |
||||||
|
'message': message |
||||||
|
})) |
||||||
@ -0,0 +1,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}" |
||||||
@ -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 |
||||||
@ -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 > 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> |
||||||
@ -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,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> |
||||||
@ -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,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'), |
||||||
|
] |
||||||
@ -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() |
||||||
@ -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