Spaces:
Sleeping
Sleeping
# --- 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'<Matiere {self.nom}>' | |
# 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'<SousCategorie {self.nom} (Matiere ID: {self.matiere_id})>' | |
# 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'<ContentBlock {self.id} (Texte ID: {self.texte_id})>' | |
# 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'<Texte {self.titre} (SousCategorie ID: {self.sous_categorie_id})>' | |
# 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'<Image {self.id} ({self.nom_fichier})>' | |
# 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'<TexteHistorique {self.id} (Texte ID: {self.texte_id})>' | |
# 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'<UserPreference {self.id} (User ID: {self.user_id})>' | |
# --------------------------------------------------------------------------- | |
# Utility Functions | |
# --------------------------------------------------------------------------- | |
# Admin authentication decorator | |
def admin_required(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 | |
# --------------------------------------------------------------------------- | |
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) | |
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]) | |
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]) | |
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 | |
}) | |
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')) | |
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}) | |
def get_image(image_id): | |
image = Image.query.get_or_404(image_id) | |
return Response(image.data, mimetype=image.mime_type) | |
# --------------------------------------------------------------------------- | |
# Admin Routes | |
# --------------------------------------------------------------------------- | |
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') | |
def logout(): | |
session.pop('admin_logged_in', None) | |
flash('Vous avez été déconnecté.', 'info') | |
return redirect(url_for('admin_bp.login')) | |
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 | |
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 | |
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 | |
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) | |
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) | |
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) | |
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 | |
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) | |