Spaces:
Sleeping
Sleeping
# START OF FILE admin.py | |
from flask import Blueprint | |
from flask_admin.contrib.sqla import ModelView | |
from flask_admin import BaseView, expose | |
from app import db, admin | |
from app.models import Matiere, SousCategorie, Texte | |
from flask_ckeditor import CKEditorField | |
from wtforms import StringField, TextAreaField | |
from bleach import clean, ALLOWED_TAGS, ALLOWED_ATTRIBUTES | |
# Importer func pour les tris/filtres | |
from sqlalchemy import func | |
bp = Blueprint('custom_admin', __name__, url_prefix='/admin') | |
# Définir les tags et attributs HTML autorisés | |
# On ajoute les tags de base + titres, listes, blockquotes etc. | |
ALLOWED_TAGS_EXTENDED = ALLOWED_TAGS + [ | |
'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', | |
'ul', 'ol', 'li', 'blockquote', 'pre', 'code', | |
'strong', 'em', 'u', 's', 'sub', 'sup', 'span' # Ajout de span pour styles potentiels | |
] | |
# Autoriser les attributs de base + 'style' pour certains éléments si nécessaire | |
# ATTENTION : Autoriser 'style' peut ouvrir des failles si pas bien contrôlé. | |
# Une alternative est de ne pas autoriser 'style' et d'utiliser des classes CSS. | |
ALLOWED_ATTRIBUTES_EXTENDED = ALLOWED_ATTRIBUTES.copy() # Créer une copie pour modifier | |
ALLOWED_ATTRIBUTES_EXTENDED['a'] = ['href', 'title', 'target'] # Autoriser target pour ouvrir dans une nouvelle fenetre | |
ALLOWED_ATTRIBUTES_EXTENDED['span'] = ['style'] # Exemple: Autoriser style sur span | |
ALLOWED_ATTRIBUTES_EXTENDED['p'] = ['style'] # Exemple: Autoriser style sur p | |
# Ajoutez d'autres éléments et leurs attributs autorisés si besoin, ex: img: ['src', 'alt', 'title', 'width', 'height'] | |
def sanitize_html(html_content): | |
""" | |
Nettoie le contenu HTML en autorisant un ensemble défini de balises et d'attributs. | |
""" | |
if not html_content: | |
return "" | |
# Utiliser bleach avec les tags/attributs étendus | |
# strip=True supprime les tags non autorisés au lieu de les échapper | |
# strip_comments=True supprime les commentaires HTML | |
cleaned_html = clean( | |
html_content, | |
tags=ALLOWED_TAGS_EXTENDED, | |
attributes=ALLOWED_ATTRIBUTES_EXTENDED, | |
strip=True, | |
strip_comments=True | |
) | |
return cleaned_html | |
class MatiereView(ModelView): | |
column_list = ('nom', 'sous_categories') # Colonnes à afficher dans la liste | |
form_columns = ('nom',) | |
# Pour trier la liste déroulante des sous-catégories (si affichée ailleurs) | |
column_sortable_list = ('nom',) | |
column_searchable_list = ('nom',) | |
class SousCategorieView(ModelView): | |
column_list = ('nom', 'matiere') | |
form_columns = ('nom', 'matiere') | |
column_sortable_list = ('nom', ('matiere', 'matiere.nom')) # Tri par nom et par nom de matière | |
column_searchable_list = ('nom', 'matiere.nom') # Recherche | |
column_filters = ('matiere',) # Ajout de filtre par matière | |
form_args = { | |
'matiere': { | |
'query_factory': lambda: Matiere.query.order_by(func.lower(Matiere.nom)) #Tri insensible à la casse | |
} | |
} | |
# Ajout de 'func' manquant dans les imports | |
def on_model_change(self, form, model, is_created): | |
# Vérification de l'unicité (nom, matiere_id) *avant* l'insertion/mise à jour | |
matiere_id = form.matiere.data.id if form.matiere.data else None | |
if not matiere_id: | |
raise ValueError("La matière est obligatoire.") | |
nom_lower = func.lower(form.nom.data) | |
query = SousCategorie.query.filter( | |
func.lower(SousCategorie.nom) == nom_lower, | |
SousCategorie.matiere_id == matiere_id | |
) | |
if not is_created: # Exclure l'enregistrement actuel lors de la mise à jour | |
query = query.filter(SousCategorie.id != model.id) | |
existing = query.first() | |
if existing: | |
raise ValueError(f"La sous-catégorie '{form.nom.data}' existe déjà pour la matière '{form.matiere.data.nom}'.") | |
class TexteView(ModelView): | |
column_list = ('titre', 'sous_categorie', 'auteur') # Ajout de l'auteur dans la liste | |
form_columns = ('titre', 'contenu', 'sous_categorie', 'auteur') # Ajout de l'auteur dans le formulaire | |
form_overrides = dict(contenu=CKEditorField) | |
column_sortable_list = ('titre', ('sous_categorie', 'sous_categorie.nom'), 'auteur') # Tri | |
column_searchable_list = ('titre', 'contenu', 'auteur', 'sous_categorie.nom') # Recherche | |
column_filters = ('sous_categorie', 'auteur') # Filtres | |
form_args = { | |
'sous_categorie': { | |
'query_factory': lambda: SousCategorie.query.join(Matiere).order_by(func.lower(Matiere.nom), func.lower(SousCategorie.nom)) | |
}, | |
'auteur': { # Optionnel: Rendre le champ auteur un peu plus petit si besoin | |
'render_kw': {'style': 'width: 300px'} | |
} | |
} | |
def on_model_change(self, form, model, is_created): | |
# Appliquer la sanitization améliorée | |
model.contenu = sanitize_html(form.contenu.data) | |
# Optionnel: Mettre une valeur par défaut si l'auteur est vide | |
if not model.auteur: | |
model.auteur = "Anonyme" # Ou None si vous préférez | |
admin.add_view(MatiereView(Matiere, db.session)) | |
admin.add_view(SousCategorieView(SousCategorie, db.session)) | |
admin.add_view(TexteView(Texte, db.session)) | |
# --- END OF FILE admin.py --- |