Compare commits
No commits in common. 'main' and 'master' have entirely different histories.
23 changed files with 152 additions and 1418 deletions
@ -1,6 +1,154 @@
@@ -1,6 +1,154 @@
|
||||
venv |
||||
# ---> Python |
||||
# 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 |
||||
__pycache__ |
||||
migrations |
||||
db.sqlite3-journal |
||||
|
||||
# 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 |
||||
.vscode |
||||
.venv |
||||
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/ |
||||
|
||||
|
||||
@ -1,36 +0,0 @@
@@ -1,36 +0,0 @@
|
||||
# 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 |
||||
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
# Fichier .env pour Django (à placer à la racine du projet) |
||||
|
||||
DJANGO_SECRET_KEY= |
||||
OPENAI_API_KEY= |
||||
DJANGO_DEBUG=1 |
||||
DJANGO_ALLOWED_HOSTS=* |
||||
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
# This file is intentionally left blank. |
||||
@ -1,13 +0,0 @@
@@ -1,13 +0,0 @@
|
||||
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 |
||||
) |
||||
), |
||||
}) |
||||
@ -1,115 +0,0 @@
@@ -1,115 +0,0 @@
|
||||
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 |
||||
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
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')), |
||||
] |
||||
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
""" |
||||
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 +0,0 @@
@@ -1 +0,0 @@
|
||||
# This file is intentionally left blank. |
||||
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
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',) |
||||
@ -1,5 +0,0 @@
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
class MainConfig(AppConfig): |
||||
default_auto_field = 'django.db.models.BigAutoField' |
||||
name = 'main' |
||||
@ -1,17 +0,0 @@
@@ -1,17 +0,0 @@
|
||||
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 |
||||
})) |
||||
@ -1,24 +0,0 @@
@@ -1,24 +0,0 @@
|
||||
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}" |
||||
@ -1,72 +0,0 @@
@@ -1,72 +0,0 @@
|
||||
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 |
||||
@ -1,218 +0,0 @@
@@ -1,218 +0,0 @@
|
||||
<!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> |
||||
@ -1,140 +0,0 @@
@@ -1,140 +0,0 @@
|
||||
<!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> |
||||
@ -1,446 +0,0 @@
@@ -1,446 +0,0 @@
|
||||
<!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> |
||||
@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
|
||||
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')) |
||||
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
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'), |
||||
] |
||||
@ -1,119 +0,0 @@
@@ -1,119 +0,0 @@
|
||||
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() |
||||
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
#!/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