Spaces:
Running
Running
Update app/admin.py
Browse files- app/admin.py +83 -26
app/admin.py
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
from flask import Blueprint
|
2 |
from flask_admin.contrib.sqla import ModelView
|
3 |
from flask_admin import BaseView, expose
|
@@ -5,66 +6,122 @@ from app import db, admin
|
|
5 |
from app.models import Matiere, SousCategorie, Texte
|
6 |
from flask_ckeditor import CKEditorField
|
7 |
from wtforms import StringField, TextAreaField
|
8 |
-
from bleach import clean
|
9 |
-
|
|
|
10 |
|
11 |
|
12 |
bp = Blueprint('custom_admin', __name__, url_prefix='/admin')
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
|
20 |
class MatiereView(ModelView):
|
21 |
column_list = ('nom', 'sous_categories') # Colonnes à afficher dans la liste
|
22 |
form_columns = ('nom',)
|
|
|
|
|
|
|
|
|
23 |
|
24 |
class SousCategorieView(ModelView):
|
25 |
column_list = ('nom', 'matiere')
|
26 |
form_columns = ('nom', 'matiere')
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
29 |
'matiere': {
|
30 |
'query_factory': lambda: Matiere.query.order_by(func.lower(Matiere.nom)) #Tri insensible à la casse
|
31 |
}
|
32 |
}
|
|
|
|
|
33 |
def on_model_change(self, form, model, is_created):
|
34 |
# Vérification de l'unicité (nom, matiere_id) *avant* l'insertion/mise à jour
|
35 |
-
if
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
51 |
|
52 |
|
53 |
class TexteView(ModelView):
|
54 |
-
column_list = ('titre', 'sous_categorie')
|
55 |
-
form_columns = ('titre', 'contenu', 'sous_categorie')
|
56 |
form_overrides = dict(contenu=CKEditorField)
|
|
|
|
|
|
|
|
|
|
|
57 |
form_args = {
|
58 |
'sous_categorie': {
|
59 |
'query_factory': lambda: SousCategorie.query.join(Matiere).order_by(func.lower(Matiere.nom), func.lower(SousCategorie.nom))
|
|
|
|
|
|
|
60 |
}
|
61 |
}
|
62 |
|
63 |
def on_model_change(self, form, model, is_created):
|
|
|
64 |
model.contenu = sanitize_html(form.contenu.data)
|
65 |
-
|
|
|
|
|
66 |
|
67 |
|
68 |
admin.add_view(MatiereView(Matiere, db.session))
|
69 |
admin.add_view(SousCategorieView(SousCategorie, db.session))
|
70 |
admin.add_view(TexteView(Texte, db.session))
|
|
|
|
|
|
1 |
+
# START OF FILE admin.py
|
2 |
from flask import Blueprint
|
3 |
from flask_admin.contrib.sqla import ModelView
|
4 |
from flask_admin import BaseView, expose
|
|
|
6 |
from app.models import Matiere, SousCategorie, Texte
|
7 |
from flask_ckeditor import CKEditorField
|
8 |
from wtforms import StringField, TextAreaField
|
9 |
+
from bleach import clean, ALLOWED_TAGS, ALLOWED_ATTRIBUTES
|
10 |
+
# Importer func pour les tris/filtres
|
11 |
+
from sqlalchemy import func
|
12 |
|
13 |
|
14 |
bp = Blueprint('custom_admin', __name__, url_prefix='/admin')
|
15 |
|
16 |
+
# Définir les tags et attributs HTML autorisés
|
17 |
+
# On ajoute les tags de base + titres, listes, blockquotes etc.
|
18 |
+
ALLOWED_TAGS_EXTENDED = ALLOWED_TAGS + [
|
19 |
+
'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
20 |
+
'ul', 'ol', 'li', 'blockquote', 'pre', 'code',
|
21 |
+
'strong', 'em', 'u', 's', 'sub', 'sup', 'span' # Ajout de span pour styles potentiels
|
22 |
+
]
|
23 |
+
|
24 |
+
# Autoriser les attributs de base + 'style' pour certains éléments si nécessaire
|
25 |
+
# ATTENTION : Autoriser 'style' peut ouvrir des failles si pas bien contrôlé.
|
26 |
+
# Une alternative est de ne pas autoriser 'style' et d'utiliser des classes CSS.
|
27 |
+
ALLOWED_ATTRIBUTES_EXTENDED = ALLOWED_ATTRIBUTES.copy() # Créer une copie pour modifier
|
28 |
+
ALLOWED_ATTRIBUTES_EXTENDED['a'] = ['href', 'title', 'target'] # Autoriser target pour ouvrir dans une nouvelle fenetre
|
29 |
+
ALLOWED_ATTRIBUTES_EXTENDED['span'] = ['style'] # Exemple: Autoriser style sur span
|
30 |
+
ALLOWED_ATTRIBUTES_EXTENDED['p'] = ['style'] # Exemple: Autoriser style sur p
|
31 |
+
# Ajoutez d'autres éléments et leurs attributs autorisés si besoin, ex: img: ['src', 'alt', 'title', 'width', 'height']
|
32 |
|
33 |
+
def sanitize_html(html_content):
|
34 |
+
"""
|
35 |
+
Nettoie le contenu HTML en autorisant un ensemble défini de balises et d'attributs.
|
36 |
+
"""
|
37 |
+
if not html_content:
|
38 |
+
return ""
|
39 |
+
|
40 |
+
# Utiliser bleach avec les tags/attributs étendus
|
41 |
+
# strip=True supprime les tags non autorisés au lieu de les échapper
|
42 |
+
# strip_comments=True supprime les commentaires HTML
|
43 |
+
cleaned_html = clean(
|
44 |
+
html_content,
|
45 |
+
tags=ALLOWED_TAGS_EXTENDED,
|
46 |
+
attributes=ALLOWED_ATTRIBUTES_EXTENDED,
|
47 |
+
strip=True,
|
48 |
+
strip_comments=True
|
49 |
+
)
|
50 |
+
return cleaned_html
|
51 |
|
52 |
|
53 |
class MatiereView(ModelView):
|
54 |
column_list = ('nom', 'sous_categories') # Colonnes à afficher dans la liste
|
55 |
form_columns = ('nom',)
|
56 |
+
# Pour trier la liste déroulante des sous-catégories (si affichée ailleurs)
|
57 |
+
column_sortable_list = ('nom',)
|
58 |
+
column_searchable_list = ('nom',)
|
59 |
+
|
60 |
|
61 |
class SousCategorieView(ModelView):
|
62 |
column_list = ('nom', 'matiere')
|
63 |
form_columns = ('nom', 'matiere')
|
64 |
+
column_sortable_list = ('nom', ('matiere', 'matiere.nom')) # Tri par nom et par nom de matière
|
65 |
+
column_searchable_list = ('nom', 'matiere.nom') # Recherche
|
66 |
+
column_filters = ('matiere',) # Ajout de filtre par matière
|
67 |
+
|
68 |
+
form_args = {
|
69 |
'matiere': {
|
70 |
'query_factory': lambda: Matiere.query.order_by(func.lower(Matiere.nom)) #Tri insensible à la casse
|
71 |
}
|
72 |
}
|
73 |
+
|
74 |
+
# Ajout de 'func' manquant dans les imports
|
75 |
def on_model_change(self, form, model, is_created):
|
76 |
# Vérification de l'unicité (nom, matiere_id) *avant* l'insertion/mise à jour
|
77 |
+
matiere_id = form.matiere.data.id if form.matiere.data else None
|
78 |
+
if not matiere_id:
|
79 |
+
raise ValueError("La matière est obligatoire.")
|
80 |
+
|
81 |
+
nom_lower = func.lower(form.nom.data)
|
82 |
+
|
83 |
+
query = SousCategorie.query.filter(
|
84 |
+
func.lower(SousCategorie.nom) == nom_lower,
|
85 |
+
SousCategorie.matiere_id == matiere_id
|
86 |
+
)
|
87 |
+
|
88 |
+
if not is_created: # Exclure l'enregistrement actuel lors de la mise à jour
|
89 |
+
query = query.filter(SousCategorie.id != model.id)
|
90 |
+
|
91 |
+
existing = query.first()
|
92 |
+
|
93 |
+
if existing:
|
94 |
+
raise ValueError(f"La sous-catégorie '{form.nom.data}' existe déjà pour la matière '{form.matiere.data.nom}'.")
|
95 |
|
96 |
|
97 |
class TexteView(ModelView):
|
98 |
+
column_list = ('titre', 'sous_categorie', 'auteur') # Ajout de l'auteur dans la liste
|
99 |
+
form_columns = ('titre', 'contenu', 'sous_categorie', 'auteur') # Ajout de l'auteur dans le formulaire
|
100 |
form_overrides = dict(contenu=CKEditorField)
|
101 |
+
|
102 |
+
column_sortable_list = ('titre', ('sous_categorie', 'sous_categorie.nom'), 'auteur') # Tri
|
103 |
+
column_searchable_list = ('titre', 'contenu', 'auteur', 'sous_categorie.nom') # Recherche
|
104 |
+
column_filters = ('sous_categorie', 'auteur') # Filtres
|
105 |
+
|
106 |
form_args = {
|
107 |
'sous_categorie': {
|
108 |
'query_factory': lambda: SousCategorie.query.join(Matiere).order_by(func.lower(Matiere.nom), func.lower(SousCategorie.nom))
|
109 |
+
},
|
110 |
+
'auteur': { # Optionnel: Rendre le champ auteur un peu plus petit si besoin
|
111 |
+
'render_kw': {'style': 'width: 300px'}
|
112 |
}
|
113 |
}
|
114 |
|
115 |
def on_model_change(self, form, model, is_created):
|
116 |
+
# Appliquer la sanitization améliorée
|
117 |
model.contenu = sanitize_html(form.contenu.data)
|
118 |
+
# Optionnel: Mettre une valeur par défaut si l'auteur est vide
|
119 |
+
if not model.auteur:
|
120 |
+
model.auteur = "Anonyme" # Ou None si vous préférez
|
121 |
|
122 |
|
123 |
admin.add_view(MatiereView(Matiere, db.session))
|
124 |
admin.add_view(SousCategorieView(SousCategorie, db.session))
|
125 |
admin.add_view(TexteView(Texte, db.session))
|
126 |
+
|
127 |
+
# --- END OF FILE admin.py ---
|