# --- START OF index.py --- import os import logging import json from flask import Flask, Blueprint, render_template, request, redirect, url_for, flash, jsonify, session, current_app, send_file, Response from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import DeclarativeBase from werkzeug.utils import secure_filename from werkzeug.security import check_password_hash, generate_password_hash from datetime import datetime from functools import wraps import requests from io import BytesIO import base64 # Configure logging logging.basicConfig(level=logging.INFO) # --------------------------------------------------------------------------- # Configuration (Hardcoded as requested) # --------------------------------------------------------------------------- # Database configuration SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') # Session secret key SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev_secret_key_change_in_production') if SECRET_KEY == 'dev_secret_key_change_in_production': print("WARNING: Using default SECRET_KEY. Set SESSION_SECRET environment variable for production.") # Admin login credentials (simple authentication for single admin) ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME', 'admin') ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'password') # Telegram configuration for feedback TELEGRAM_BOT_TOKEN = "7126991043:AAEzeKswNo6eO7oJA49Hxn_bsbzgzUoJ-6A" TELEGRAM_CHAT_ID = "-1002081124539" # Application host/port/debug DEBUG = os.environ.get('DEBUG', 'False') == 'True' HOST = '0.0.0.0' PORT = int(os.environ.get('PORT', 5000)) # --------------------------------------------------------------------------- # Flask App Initialization and SQLAlchemy Setup # --------------------------------------------------------------------------- # Create base class for SQLAlchemy models class Base(DeclarativeBase): pass # Initialize SQLAlchemy with the Base class db = SQLAlchemy(model_class=Base) # Create Flask application app = Flask(__name__) # Apply configuration app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI app.config['SECRET_KEY'] = SECRET_KEY app.config['ADMIN_USERNAME'] = ADMIN_USERNAME app.config['ADMIN_PASSWORD'] = ADMIN_PASSWORD app.config['TELEGRAM_BOT_TOKEN'] = TELEGRAM_BOT_TOKEN app.config['TELEGRAM_CHAT_ID'] = TELEGRAM_CHAT_ID app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_ENGINE_OPTIONS"] = { "pool_recycle": 300, "pool_pre_ping": True, } app.config['HOST'] = HOST app.config['PORT'] = PORT app.config['DEBUG'] = DEBUG app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload # Initialize the app with SQLAlchemy db.init_app(app) # --------------------------------------------------------------------------- # Database Models # --------------------------------------------------------------------------- # Matiere (Subject) Model class Matiere(db.Model): id = db.Column(db.Integer, primary_key=True) nom = db.Column(db.String(100), unique=True, nullable=False) color_code = db.Column(db.String(7), nullable=False, default="#3498db") # Add color code field for subject # Relationships sous_categories = db.relationship('SousCategorie', backref='matiere', lazy=True, cascade="all, delete-orphan") def __repr__(self): return f'' # SousCategorie (SubCategory) Model class SousCategorie(db.Model): id = db.Column(db.Integer, primary_key=True) nom = db.Column(db.String(100), nullable=False) matiere_id = db.Column(db.Integer, db.ForeignKey('matiere.id'), nullable=False) # Enforce unique constraint on name within same matiere __table_args__ = (db.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc'),) # Relationships textes = db.relationship('Texte', backref='sous_categorie', lazy=True, cascade="all, delete-orphan") def __repr__(self): return f'' # ContentBlock model for the new block-based content class ContentBlock(db.Model): id = db.Column(db.Integer, primary_key=True) texte_id = db.Column(db.Integer, db.ForeignKey('texte.id'), nullable=False) title = db.Column(db.String(200), nullable=True) content = db.Column(db.Text, nullable=False) order = db.Column(db.Integer, nullable=False, default=0) image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) image_position = db.Column(db.String(10), nullable=True, default='left') # 'left', 'right', 'top', 'bottom' # Relationship image = db.relationship('Image', foreign_keys=[image_id]) def __repr__(self): return f'' # Texte (Text content) Model class Texte(db.Model): id = db.Column(db.Integer, primary_key=True) titre = db.Column(db.String(200), nullable=False) contenu = db.Column(db.Text, nullable=False) sous_categorie_id = db.Column(db.Integer, db.ForeignKey('sous_categorie.id'), nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships historiques = db.relationship('TexteHistorique', backref='texte', lazy=True, cascade="all, delete-orphan") content_blocks = db.relationship('ContentBlock', backref='texte', lazy=True, cascade="all, delete-orphan", order_by="ContentBlock.order") def __repr__(self): return f'' # Image Model for storing content images class Image(db.Model): id = db.Column(db.Integer, primary_key=True) nom_fichier = db.Column(db.String(255)) mime_type = db.Column(db.String(100), nullable=False) data = db.Column(db.LargeBinary, nullable=False) # BLOB/BYTEA for image data uploaded_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Additional fields for image management description = db.Column(db.String(255), nullable=True) alt_text = db.Column(db.String(255), nullable=True) def __repr__(self): return f'' # TexteHistorique (Text History) Model class TexteHistorique(db.Model): id = db.Column(db.Integer, primary_key=True) texte_id = db.Column(db.Integer, db.ForeignKey('texte.id'), nullable=False) contenu_precedent = db.Column(db.Text, nullable=False) date_modification = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) def __repr__(self): return f'' # UserPreference Model to store user theme preferences class UserPreference(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(50), unique=True, nullable=False) # Use session ID or similar for anonymous users theme = db.Column(db.String(10), nullable=False, default='light') # 'light' or 'dark' created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) def __repr__(self): return f'' # --------------------------------------------------------------------------- # Utility Functions # --------------------------------------------------------------------------- # Admin authentication decorator def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not session.get('admin_logged_in'): flash('Veuillez vous connecter pour accéder à cette page.', 'warning') return redirect(url_for('admin_bp.login')) return f(*args, **kwargs) return decorated_function # Admin login check def check_admin_credentials(username, password): # Access config via current_app proxy admin_username = current_app.config['ADMIN_USERNAME'] admin_password = current_app.config['ADMIN_PASSWORD'] return username == admin_username and password == admin_password # Send feedback to Telegram def send_telegram_feedback(message): token = current_app.config.get('TELEGRAM_BOT_TOKEN') chat_id = current_app.config.get('TELEGRAM_CHAT_ID') if not token or not chat_id: current_app.logger.error("Telegram bot token or chat ID not configured") return False api_url = f"https://api.telegram.org/bot{token}/sendMessage" payload = { "chat_id": chat_id, "text": f"📝 Nouveau feedback:\n\n{message}", "parse_mode": "HTML" } try: response = requests.post(api_url, data=payload, timeout=10) response.raise_for_status() current_app.logger.info("Feedback sent to Telegram successfully") return True except requests.exceptions.RequestException as e: current_app.logger.error(f"Error sending feedback to Telegram: {str(e)}") if hasattr(e, 'response') and e.response is not None: current_app.logger.error(f"Telegram API Response: {e.response.text}") return False except Exception as e: current_app.logger.error(f"Unexpected exception while sending feedback to Telegram: {str(e)}") return False # Get or create user preferences def get_user_preferences(): user_id = session.get('user_id') if not user_id: # Generate a unique ID for new users user_id = str(datetime.utcnow().timestamp()) session['user_id'] = user_id user_pref = UserPreference.query.filter_by(user_id=user_id).first() if not user_pref: user_pref = UserPreference(user_id=user_id) db.session.add(user_pref) db.session.commit() return user_pref # Parse text content into blocks def parse_content_to_blocks(text_content): # Simple parser that creates blocks based on paragraphs or headings blocks = [] current_block = {"title": None, "content": ""} for line in text_content.split('\n'): line = line.strip() if not line: # Empty line might indicate a block break if current_block["content"]: blocks.append(current_block) current_block = {"title": None, "content": ""} continue # Check if line might be a heading (simplistic approach) if len(line) < 100 and not current_block["content"]: current_block["title"] = line else: if current_block["content"]: current_block["content"] += "\n" + line else: current_block["content"] = line # Add the last block if not empty if current_block["content"]: blocks.append(current_block) # If no blocks were created, create one with all content if not blocks: blocks.append({"title": None, "content": text_content}) return blocks # --------------------------------------------------------------------------- # Blueprints Definition # --------------------------------------------------------------------------- main_bp = Blueprint('main_bp', __name__) admin_bp = Blueprint('admin_bp', __name__, url_prefix='/gestion') # --------------------------------------------------------------------------- # Main Routes # --------------------------------------------------------------------------- @main_bp.route('/') def index(): # Get user theme preference user_pref = get_user_preferences() # Fetch all subjects (matieres) matieres = Matiere.query.all() return render_template('index.html', matieres=matieres, theme=user_pref.theme) @main_bp.route('/get_sous_categories/') def get_sous_categories(matiere_id): sous_categories = SousCategorie.query.filter_by(matiere_id=matiere_id).all() return jsonify([{'id': sc.id, 'nom': sc.nom} for sc in sous_categories]) @main_bp.route('/get_textes/') def get_textes(sous_categorie_id): textes = Texte.query.filter_by(sous_categorie_id=sous_categorie_id).all() return jsonify([{'id': t.id, 'titre': t.titre} for t in textes]) @main_bp.route('/get_texte/') def get_texte(texte_id): texte = Texte.query.get_or_404(texte_id) # Get the subject color for theming matiere = Matiere.query.join(SousCategorie).filter(SousCategorie.id == texte.sous_categorie_id).first() color_code = matiere.color_code if matiere else "#3498db" # Check if the texte has content blocks if texte.content_blocks: blocks = [] for block in texte.content_blocks: block_data = { 'id': block.id, 'title': block.title, 'content': block.content, 'order': block.order, 'image_position': block.image_position, 'image': None } # Add image data if available if block.image: image_data = base64.b64encode(block.image.data).decode('utf-8') block_data['image'] = { 'id': block.image.id, 'src': f"data:{block.image.mime_type};base64,{image_data}", 'alt': block.image.alt_text or block.title or "Image illustration" } blocks.append(block_data) else: # If no blocks exist yet, parse the content to create blocks # This is useful for existing content migration parsed_blocks = parse_content_to_blocks(texte.contenu) blocks = [] for i, block_data in enumerate(parsed_blocks): blocks.append({ 'id': None, 'title': block_data['title'], 'content': block_data['content'], 'order': i, 'image_position': 'left', 'image': None }) return jsonify({ 'id': texte.id, 'titre': texte.titre, 'contenu': texte.contenu, 'blocks': blocks, 'color_code': color_code }) @main_bp.route('/feedback', methods=['POST']) def submit_feedback(): message = request.form.get('message', '').strip() if not message: flash("Le message ne peut pas être vide.", "error") return redirect(url_for('main_bp.index')) success = send_telegram_feedback(message) if success: flash("Merci pour votre feedback!", "success") else: flash("Une erreur s'est produite lors de l'envoi de votre feedback. Veuillez réessayer plus tard.", "error") return redirect(url_for('main_bp.index')) @main_bp.route('/set_theme', methods=['POST']) def set_theme(): theme = request.form.get('theme', 'light') if theme not in ['light', 'dark']: theme = 'light' user_pref = get_user_preferences() user_pref.theme = theme db.session.commit() return jsonify({'success': True, 'theme': theme}) @main_bp.route('/image/') def get_image(image_id): image = Image.query.get_or_404(image_id) return Response(image.data, mimetype=image.mime_type) # --------------------------------------------------------------------------- # Admin Routes # --------------------------------------------------------------------------- @admin_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if check_admin_credentials(username, password): session['admin_logged_in'] = True flash('Connexion réussie !', 'success') return redirect(url_for('admin_bp.dashboard')) else: flash('Nom d\'utilisateur ou mot de passe incorrect.', 'danger') return render_template('admin/login.html') @admin_bp.route('/logout') def logout(): session.pop('admin_logged_in', None) flash('Vous avez été déconnecté.', 'info') return redirect(url_for('admin_bp.login')) @admin_bp.route('/') @admin_required def dashboard(): # Count of each entity type for dashboard stats stats = { 'matieres': Matiere.query.count(), 'sous_categories': SousCategorie.query.count(), 'textes': Texte.query.count(), 'images': Image.query.count() } # Get recent texts for dashboard recent_textes = Texte.query.order_by(Texte.updated_at.desc()).limit(5).all() return render_template('admin/dashboard.html', stats=stats, recent_textes=recent_textes) # Matières (Subjects) Management @admin_bp.route('/matieres', methods=['GET', 'POST']) @admin_required def matieres(): if request.method == 'POST': action = request.form.get('action') if action == 'add': nom = request.form.get('nom', '').strip() color_code = request.form.get('color_code', '#3498db') if not nom: flash('Le nom de la matière est requis.', 'danger') else: matiere = Matiere.query.filter_by(nom=nom).first() if matiere: flash(f'La matière "{nom}" existe déjà.', 'warning') else: new_matiere = Matiere(nom=nom, color_code=color_code) db.session.add(new_matiere) db.session.commit() flash(f'Matière "{nom}" ajoutée avec succès !', 'success') elif action == 'edit': matiere_id = request.form.get('matiere_id') nom = request.form.get('nom', '').strip() color_code = request.form.get('color_code', '#3498db') if not matiere_id or not nom: flash('Informations incomplètes pour la modification.', 'danger') else: matiere = Matiere.query.get(matiere_id) if not matiere: flash('Matière non trouvée.', 'danger') else: existing = Matiere.query.filter_by(nom=nom).first() if existing and existing.id != int(matiere_id): flash(f'Une autre matière avec le nom "{nom}" existe déjà.', 'warning') else: matiere.nom = nom matiere.color_code = color_code db.session.commit() flash(f'Matière "{nom}" modifiée avec succès !', 'success') elif action == 'delete': matiere_id = request.form.get('matiere_id') if not matiere_id: flash('ID de matière manquant pour la suppression.', 'danger') else: matiere = Matiere.query.get(matiere_id) if not matiere: flash('Matière non trouvée.', 'danger') else: nom = matiere.nom db.session.delete(matiere) db.session.commit() flash(f'Matière "{nom}" supprimée avec succès !', 'success') matieres = Matiere.query.all() return render_template('admin/matieres.html', matieres=matieres) # Sous-Catégories (Subcategories) Management @admin_bp.route('/sous_categories', methods=['GET', 'POST']) @admin_required def sous_categories(): if request.method == 'POST': action = request.form.get('action') if action == 'add': nom = request.form.get('nom', '').strip() matiere_id = request.form.get('matiere_id') if not nom or not matiere_id: flash('Le nom et la matière sont requis.', 'danger') else: sous_categorie = SousCategorie.query.filter_by(nom=nom, matiere_id=matiere_id).first() if sous_categorie: flash(f'La sous-catégorie "{nom}" existe déjà pour cette matière.', 'warning') else: new_sous_categorie = SousCategorie(nom=nom, matiere_id=matiere_id) db.session.add(new_sous_categorie) db.session.commit() flash(f'Sous-catégorie "{nom}" ajoutée avec succès !', 'success') elif action == 'edit': sous_categorie_id = request.form.get('sous_categorie_id') nom = request.form.get('nom', '').strip() matiere_id = request.form.get('matiere_id') if not sous_categorie_id or not nom or not matiere_id: flash('Informations incomplètes pour la modification.', 'danger') else: sous_categorie = SousCategorie.query.get(sous_categorie_id) if not sous_categorie: flash('Sous-catégorie non trouvée.', 'danger') else: existing = SousCategorie.query.filter_by(nom=nom, matiere_id=matiere_id).first() if existing and existing.id != int(sous_categorie_id): flash(f'Une sous-catégorie avec le nom "{nom}" existe déjà pour cette matière.', 'warning') else: sous_categorie.nom = nom sous_categorie.matiere_id = matiere_id db.session.commit() flash(f'Sous-catégorie "{nom}" modifiée avec succès !', 'success') elif action == 'delete': sous_categorie_id = request.form.get('sous_categorie_id') if not sous_categorie_id: flash('ID de sous-catégorie manquant pour la suppression.', 'danger') else: sous_categorie = SousCategorie.query.get(sous_categorie_id) if not sous_categorie: flash('Sous-catégorie non trouvée.', 'danger') else: nom = sous_categorie.nom db.session.delete(sous_categorie) db.session.commit() flash(f'Sous-catégorie "{nom}" supprimée avec succès !', 'success') sous_categories = SousCategorie.query.join(Matiere).all() matieres = Matiere.query.all() return render_template('admin/sous_categories.html', sous_categories=sous_categories, matieres=matieres) # Textes (Content) Management @admin_bp.route('/textes', methods=['GET', 'POST']) @admin_required def textes(): if request.method == 'POST': action = request.form.get('action') if action == 'add': titre = request.form.get('titre', '').strip() sous_categorie_id = request.form.get('sous_categorie_id') contenu = request.form.get('contenu', '').strip() if not titre or not sous_categorie_id or not contenu: flash('Tous les champs sont requis.', 'danger') else: new_texte = Texte( titre=titre, sous_categorie_id=sous_categorie_id, contenu=contenu ) db.session.add(new_texte) db.session.commit() # Parse content into blocks blocks = parse_content_to_blocks(contenu) for i, block_data in enumerate(blocks): new_block = ContentBlock( texte_id=new_texte.id, title=block_data['title'], content=block_data['content'], order=i ) db.session.add(new_block) db.session.commit() flash(f'Texte "{titre}" ajouté avec succès !', 'success') return redirect(url_for('admin_bp.edit_texte', texte_id=new_texte.id)) elif action == 'delete': texte_id = request.form.get('texte_id') if not texte_id: flash('ID de texte manquant pour la suppression.', 'danger') else: texte = Texte.query.get(texte_id) if not texte: flash('Texte non trouvé.', 'danger') else: titre = texte.titre db.session.delete(texte) db.session.commit() flash(f'Texte "{titre}" supprimé avec succès !', 'success') matieres = Matiere.query.all() textes = Texte.query.join(SousCategorie).join(Matiere).order_by(Matiere.nom, SousCategorie.nom, Texte.titre).all() # Group texts by matiere and sous_categorie for easier display grouped_textes = {} for texte in textes: matiere_id = texte.sous_categorie.matiere.id if matiere_id not in grouped_textes: grouped_textes[matiere_id] = { 'nom': texte.sous_categorie.matiere.nom, 'color': texte.sous_categorie.matiere.color_code, 'sous_categories': {} } sous_cat_id = texte.sous_categorie.id if sous_cat_id not in grouped_textes[matiere_id]['sous_categories']: grouped_textes[matiere_id]['sous_categories'][sous_cat_id] = { 'nom': texte.sous_categorie.nom, 'textes': [] } grouped_textes[matiere_id]['sous_categories'][sous_cat_id]['textes'].append({ 'id': texte.id, 'titre': texte.titre, 'updated_at': texte.updated_at }) sous_categories = SousCategorie.query.all() return render_template('admin/textes.html', grouped_textes=grouped_textes, matieres=matieres, sous_categories=sous_categories) @admin_bp.route('/textes/edit/', methods=['GET', 'POST']) @admin_required def edit_texte(texte_id): texte = Texte.query.get_or_404(texte_id) if request.method == 'POST': action = request.form.get('action') if action == 'update_basic': # Basic text info update titre = request.form.get('titre', '').strip() sous_categorie_id = request.form.get('sous_categorie_id') if not titre or not sous_categorie_id: flash('Le titre et la sous-catégorie sont requis.', 'danger') else: # Save previous content for history historique = TexteHistorique( texte_id=texte.id, contenu_precedent=texte.contenu ) db.session.add(historique) texte.titre = titre texte.sous_categorie_id = sous_categorie_id db.session.commit() flash('Informations de base mises à jour avec succès !', 'success') elif action == 'update_blocks': # Update content blocks blocks_data = json.loads(request.form.get('blocks_data', '[]')) # Save previous content for history if changes were made historique = TexteHistorique( texte_id=texte.id, contenu_precedent=texte.contenu ) db.session.add(historique) # Update all blocks # First, remove existing blocks for block in texte.content_blocks: db.session.delete(block) # Then create new blocks from the submitted data new_content = [] for i, block_data in enumerate(blocks_data): new_block = ContentBlock( texte_id=texte.id, title=block_data.get('title'), content=block_data.get('content', ''), order=i, image_position=block_data.get('image_position', 'left') ) # Set image if provided image_id = block_data.get('image_id') if image_id and image_id != 'null' and image_id != 'undefined': new_block.image_id = image_id db.session.add(new_block) # Append to full content for the main text field if new_block.title: new_content.append(new_block.title) new_content.append(new_block.content) # Update the main content field to match block content texte.contenu = "\n\n".join(new_content) db.session.commit() flash('Contenu mis à jour avec succès !', 'success') elif action == 'upload_image': if 'image' not in request.files: return jsonify({'success': False, 'error': 'Aucun fichier trouvé'}) file = request.files['image'] if file.filename == '': return jsonify({'success': False, 'error': 'Aucun fichier sélectionné'}) # Check file type (accept only images) allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'] if file.mimetype not in allowed_mimetypes: return jsonify({'success': False, 'error': 'Type de fichier non autorisé'}) # Read file data file_data = file.read() if not file_data: return jsonify({'success': False, 'error': 'Fichier vide'}) # Create new image record new_image = Image( nom_fichier=secure_filename(file.filename), mime_type=file.mimetype, data=file_data, alt_text=request.form.get('alt_text', '') ) db.session.add(new_image) db.session.commit() # Return image details image_data = base64.b64encode(new_image.data).decode('utf-8') return jsonify({ 'success': True, 'image': { 'id': new_image.id, 'filename': new_image.nom_fichier, 'src': f"data:{new_image.mime_type};base64,{image_data}", 'alt': new_image.alt_text or "Image illustration" } }) # Get existing content blocks or create them if none exist if not texte.content_blocks: # Parse content into blocks blocks = parse_content_to_blocks(texte.contenu) for i, block_data in enumerate(blocks): new_block = ContentBlock( texte_id=texte.id, title=block_data['title'], content=block_data['content'], order=i ) db.session.add(new_block) db.session.commit() # Reload the texte to get the new blocks texte = Texte.query.get(texte_id) # Prepare block data for template blocks = [] for block in texte.content_blocks: block_data = { 'id': block.id, 'title': block.title, 'content': block.content, 'order': block.order, 'image_position': block.image_position, 'image': None } # Add image data if available if block.image: image_data = base64.b64encode(block.image.data).decode('utf-8') block_data['image'] = { 'id': block.image.id, 'src': f"data:{block.image.mime_type};base64,{image_data}", 'alt': block.image.alt_text or block.title or "Image illustration" } blocks.append(block_data) # Get all images for selection images = Image.query.order_by(Image.uploaded_at.desc()).all() images_data = [] for image in images: image_data = base64.b64encode(image.data).decode('utf-8') images_data.append({ 'id': image.id, 'filename': image.nom_fichier, 'src': f"data:{image.mime_type};base64,{image_data}", 'alt': image.alt_text or "Image illustration" }) sous_categories = SousCategorie.query.all() return render_template('admin/edit_texte.html', texte=texte, blocks=blocks, images=images_data, sous_categories=sous_categories) @admin_bp.route('/historique/') @admin_required def historique(texte_id): texte = Texte.query.get_or_404(texte_id) historiques = TexteHistorique.query.filter_by(texte_id=texte_id).order_by(TexteHistorique.date_modification.desc()).all() return render_template('admin/historique.html', texte=texte, historiques=historiques) @admin_bp.route('/images', methods=['GET', 'POST']) @admin_required def images(): if request.method == 'POST': action = request.form.get('action') if action == 'upload': if 'image' not in request.files: flash('Aucun fichier trouvé.', 'danger') return redirect(request.url) file = request.files['image'] if file.filename == '': flash('Aucun fichier sélectionné.', 'danger') return redirect(request.url) # Check file type (accept only images) allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'] if file.mimetype not in allowed_mimetypes: flash('Type de fichier non autorisé.', 'danger') return redirect(request.url) # Read file data file_data = file.read() if not file_data: flash('Fichier vide.', 'danger') return redirect(request.url) # Create new image record alt_text = request.form.get('alt_text', '') description = request.form.get('description', '') new_image = Image( nom_fichier=secure_filename(file.filename), mime_type=file.mimetype, data=file_data, alt_text=alt_text, description=description ) db.session.add(new_image) db.session.commit() flash('Image téléchargée avec succès !', 'success') elif action == 'delete': image_id = request.form.get('image_id') if image_id: image = Image.query.get(image_id) if image: # Check if image is used in any content block usage_count = ContentBlock.query.filter_by(image_id=image.id).count() if usage_count > 0: flash(f'Cette image est utilisée dans {usage_count} blocs de contenu. Veuillez les modifier avant de supprimer l\'image.', 'warning') else: db.session.delete(image) db.session.commit() flash('Image supprimée avec succès !', 'success') else: flash('Image non trouvée.', 'danger') else: flash('ID d\'image manquant.', 'danger') elif action == 'update': image_id = request.form.get('image_id') alt_text = request.form.get('alt_text', '') description = request.form.get('description', '') if image_id: image = Image.query.get(image_id) if image: image.alt_text = alt_text image.description = description db.session.commit() flash('Informations de l\'image mises à jour avec succès !', 'success') else: flash('Image non trouvée.', 'danger') else: flash('ID d\'image manquant.', 'danger') # Get all images images = Image.query.order_by(Image.uploaded_at.desc()).all() images_data = [] for image in images: image_data = base64.b64encode(image.data).decode('utf-8') images_data.append({ 'id': image.id, 'filename': image.nom_fichier, 'description': image.description, 'alt_text': image.alt_text, 'uploaded_at': image.uploaded_at, 'src': f"data:{image.mime_type};base64,{image_data}" }) return render_template('admin/images.html', images=images_data) # --------------------------------------------------------------------------- # Register blueprints and setup database # --------------------------------------------------------------------------- app.register_blueprint(main_bp) app.register_blueprint(admin_bp) # Create tables if they don't exist with app.app_context(): db.create_all() # Application entry point @app.route('/') def index(): return redirect(url_for('main_bp.index')) # --------------------------------------------------------------------------- # Serve the app - This is only used when running locally # --------------------------------------------------------------------------- if __name__ == '__main__': app.run(host=HOST, port=PORT, debug=DEBUG) # For Vercel serverless function def app_handler(environ, start_response): return app(environ, start_response)