Mariam-courss / api /index.py
kuro223's picture
89192
a1be761
# --- 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):
@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/<int:matiere_id>')
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/<int:sous_categorie_id>')
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/<int:texte_id>')
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/<int:image_id>')
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/<int:texte_id>', 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/<int:texte_id>')
@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)