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 @@ |
|||||||
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 |
db.sqlite3 |
||||||
__pycache__ |
db.sqlite3-journal |
||||||
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 |
||||||
.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 @@ |
|||||||
# 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 @@ |
|||||||
# Fichier .env pour Django (à placer à la racine du projet) |
|
||||||
|
|
||||||
DJANGO_SECRET_KEY= |
|
||||||
OPENAI_API_KEY= |
|
||||||
DJANGO_DEBUG=1 |
|
||||||
DJANGO_ALLOWED_HOSTS=* |
|
||||||
@ -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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
""" |
|
||||||
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,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 @@ |
|||||||
from django.apps import AppConfig |
|
||||||
|
|
||||||
class MainConfig(AppConfig): |
|
||||||
default_auto_field = 'django.db.models.BigAutoField' |
|
||||||
name = 'main' |
|
||||||
@ -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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
<!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 @@ |
|||||||
<!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 @@ |
|||||||
<!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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
#!/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