742 lines
27 KiB
Python
742 lines
27 KiB
Python
from flask import Flask, render_template, request, redirect, url_for, send_file, flash, session
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from functools import wraps
|
|
import sqlite3
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
from io import BytesIO
|
|
|
|
app = Flask(__name__)
|
|
|
|
# ✅ SÉCURITÉ : Clé secrète depuis variable d'environnement
|
|
app.secret_key = os.environ.get('SECRET_KEY', 'changez-moi-en-production-utilisez-env')
|
|
app.config['SESSION_COOKIE_SECURE'] = False # ✅ Changé pour HTTP (mettre True avec HTTPS)
|
|
app.config['SESSION_COOKIE_HTTPONLY'] = True # Protection XSS
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Protection CSRF
|
|
|
|
# ✅ Configuration base de données compatible Docker
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), 'data', 'menu_miam.db')
|
|
|
|
# ✅ CREDENTIALS depuis variables d'environnement
|
|
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME', 'admin')
|
|
ADMIN_PASSWORD_HASH = generate_password_hash(
|
|
os.environ.get('ADMIN_PASSWORD', 'changeme123')
|
|
)
|
|
|
|
# ========== DÉCORATEUR D'AUTHENTIFICATION ==========
|
|
def login_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'logged_in' not in session:
|
|
flash('⚠️ Vous devez vous connecter pour accéder à cette page', 'warning')
|
|
return redirect(url_for('login', next=request.url))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
# ========== ROUTES D'AUTHENTIFICATION ==========
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
# Si déjà connecté, rediriger vers le menu
|
|
if 'logged_in' in session:
|
|
return redirect(url_for('menu'))
|
|
|
|
if request.method == 'POST':
|
|
username = request.form.get('username', '').strip()
|
|
password = request.form.get('password', '')
|
|
|
|
if username == ADMIN_USERNAME and check_password_hash(ADMIN_PASSWORD_HASH, password):
|
|
session['logged_in'] = True
|
|
session['username'] = username
|
|
flash('✅ Connexion réussie !', 'success')
|
|
|
|
# ✅ CORRECTION : Gérer correctement la redirection
|
|
next_page = request.args.get('next')
|
|
|
|
# Vérifier que next_page est un chemin relatif valide
|
|
if next_page and next_page.startswith('/') and not next_page.startswith('//'):
|
|
return redirect(next_page)
|
|
|
|
# Sinon, rediriger vers le menu par défaut
|
|
return redirect(url_for('menu'))
|
|
else:
|
|
flash('❌ Identifiants incorrects', 'error')
|
|
|
|
return render_template('login.html')
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
session.clear()
|
|
flash('✅ Déconnexion réussie', 'success')
|
|
return redirect(url_for('login'))
|
|
|
|
# ========== CONNEXION BASE DE DONNÉES ==========
|
|
def get_connection():
|
|
"""Connexion à la base de données avec chemin persistant"""
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
# ========== INITIALISATION BASE DE DONNÉES ==========
|
|
def init_db():
|
|
"""Créer le dossier data et initialiser la base de données"""
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
|
|
conn = get_connection()
|
|
|
|
# ✅ Table recettes
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS recettes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
nom TEXT NOT NULL,
|
|
ingredients TEXT,
|
|
instructions TEXT,
|
|
lien TEXT
|
|
)
|
|
''')
|
|
|
|
# ✅ Table menu
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS menu (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
date TEXT NOT NULL,
|
|
jour TEXT NOT NULL,
|
|
repas TEXT NOT NULL,
|
|
recette_id INTEGER,
|
|
FOREIGN KEY (recette_id) REFERENCES recettes (id),
|
|
UNIQUE(date, jour, repas)
|
|
)
|
|
''')
|
|
|
|
# ✅ Table accompagnements du menu (MODIFIÉ - ajout de la colonne date)
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS menu_accompagnements (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
date TEXT NOT NULL,
|
|
jour TEXT NOT NULL,
|
|
repas TEXT NOT NULL,
|
|
accompagnement_id INTEGER NOT NULL,
|
|
FOREIGN KEY (accompagnement_id) REFERENCES accompagnements (id) ON DELETE CASCADE,
|
|
UNIQUE(date, jour, repas, accompagnement_id)
|
|
)
|
|
''')
|
|
|
|
# ✅ Table accompagnements
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS accompagnements (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
nom TEXT NOT NULL UNIQUE,
|
|
descriptif TEXT
|
|
)
|
|
''')
|
|
|
|
# ✅ Table de liaison recette-accompagnements (pour suggestions par défaut)
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS recette_accompagnements (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
recette_id INTEGER NOT NULL,
|
|
accompagnement_id INTEGER NOT NULL,
|
|
FOREIGN KEY (recette_id) REFERENCES recettes (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (accompagnement_id) REFERENCES accompagnements (id) ON DELETE CASCADE,
|
|
UNIQUE(recette_id, accompagnement_id)
|
|
)
|
|
''')
|
|
|
|
# ✅ Ajouter des recettes d'exemple si la base est vide
|
|
count = conn.execute('SELECT COUNT(*) FROM recettes').fetchone()[0]
|
|
|
|
if count == 0:
|
|
recettes_exemple = [
|
|
("Pâtes Carbonara",
|
|
"Pâtes (400g), Lardons (200g), Œufs (4), Parmesan (100g), Crème fraîche (20cl)",
|
|
"1. Cuire les pâtes al dente\n2. Faire revenir les lardons\n3. Mélanger œufs, parmesan et crème\n4. Incorporer aux pâtes chaudes\n5. Servir immédiatement",
|
|
""),
|
|
|
|
("Salade César",
|
|
"Salade romaine (1), Poulet grillé (300g), Croûtons (100g), Parmesan (50g), Sauce césar (15cl)",
|
|
"1. Laver et couper la salade\n2. Couper le poulet en lamelles\n3. Ajouter les croûtons et le parmesan\n4. Arroser de sauce césar\n5. Mélanger délicatement",
|
|
""),
|
|
|
|
("Tiramisu",
|
|
"Mascarpone (500g), Œufs (6), Sucre (150g), Café fort (30cl), Biscuits cuillère (400g), Cacao en poudre",
|
|
"1. Préparer le café et le laisser refroidir\n2. Séparer blancs et jaunes d'œufs\n3. Mélanger jaunes avec sucre et mascarpone\n4. Monter les blancs en neige et incorporer\n5. Tremper biscuits dans le café\n6. Alterner couches de biscuits et crème\n7. Saupoudrer de cacao\n8. Réfrigérer 4h minimum",
|
|
""),
|
|
|
|
("Poulet rôti",
|
|
"Poulet entier (1,5kg), Beurre (50g), Citron (1), Thym, Ail (3 gousses), Pommes de terre (1kg)",
|
|
"1. Préchauffer four à 200°C\n2. Farcir le poulet avec citron, thym et ail\n3. Badigeonner de beurre\n4. Disposer pommes de terre autour\n5. Cuire 1h15, arroser régulièrement",
|
|
""),
|
|
|
|
("Quiche Lorraine",
|
|
"Pâte brisée (1), Lardons (200g), Œufs (4), Crème fraîche (30cl), Gruyère râpé (100g), Noix de muscade",
|
|
"1. Préchauffer four à 180°C\n2. Étaler la pâte dans un moule\n3. Faire revenir les lardons\n4. Mélanger œufs, crème et gruyère\n5. Ajouter lardons et muscade\n6. Verser sur la pâte\n7. Cuire 35-40 minutes",
|
|
"")
|
|
]
|
|
|
|
try:
|
|
conn.executemany('''
|
|
INSERT INTO recettes (nom, ingredients, instructions, lien)
|
|
VALUES (?, ?, ?, ?)
|
|
''', recettes_exemple)
|
|
|
|
conn.commit()
|
|
print("✅ Base de données initialisée avec succès!")
|
|
print(f"✅ {len(recettes_exemple)} recettes d'exemple ajoutées!")
|
|
|
|
except sqlite3.IntegrityError as e:
|
|
print(f"⚠️ Certaines recettes existent déjà : {e}")
|
|
else:
|
|
print(f"✅ Base de données déjà initialisée ({count} recettes)")
|
|
|
|
# ✅ Ajouter des accompagnements d'exemple
|
|
count_acc = conn.execute('SELECT COUNT(*) FROM accompagnements').fetchone()[0]
|
|
|
|
if count_acc == 0:
|
|
accompagnements_exemple = [
|
|
("Riz blanc", "Riz basmati ou thaï"),
|
|
("Pâtes", "Pâtes italiennes variées"),
|
|
("Frites", "Pommes de terre frites maison"),
|
|
("Salade verte", "Salade fraîche de saison"),
|
|
("Légumes vapeur", "Carottes, brocolis, haricots verts"),
|
|
("Purée", "Purée de pommes de terre maison"),
|
|
("Ratatouille", "Légumes du soleil mijotés")
|
|
]
|
|
|
|
try:
|
|
conn.executemany('''
|
|
INSERT INTO accompagnements (nom, descriptif)
|
|
VALUES (?, ?)
|
|
''', accompagnements_exemple)
|
|
conn.commit()
|
|
print(f"✅ {len(accompagnements_exemple)} accompagnements d'exemple ajoutés!")
|
|
except sqlite3.IntegrityError as e:
|
|
print(f"⚠️ Certains accompagnements existent déjà : {e}")
|
|
|
|
conn.close()
|
|
|
|
# ✅ INITIALISATION AU DÉMARRAGE
|
|
init_db()
|
|
|
|
# ========== ROUTES PROTÉGÉES ==========
|
|
@app.route('/menu')
|
|
@app.route('/')
|
|
@login_required
|
|
def menu():
|
|
offset = int(request.args.get('offset', 0))
|
|
|
|
conn = get_connection()
|
|
|
|
# ✅ MODIFIÉ : Calculer les dates de la semaine AVANT la requête
|
|
today = datetime.now().date() + timedelta(weeks=offset)
|
|
start_of_week = today - timedelta(days=today.weekday())
|
|
end_of_week = start_of_week + timedelta(days=6)
|
|
|
|
# ✅ MODIFIÉ : Récupération du menu pour LA SEMAINE ACTUELLE UNIQUEMENT
|
|
menu_data = conn.execute('''
|
|
SELECT m.date, m.jour, m.repas, m.recette_id, r.nom, r.ingredients, r.lien
|
|
FROM menu m
|
|
LEFT JOIN recettes r ON m.recette_id = r.id
|
|
WHERE m.date BETWEEN ? AND ?
|
|
''', (start_of_week.isoformat(), end_of_week.isoformat())).fetchall()
|
|
|
|
menu = {}
|
|
for row in menu_data:
|
|
key = f"{row['jour']}_{row['repas']}"
|
|
menu[key] = {
|
|
'id': row['recette_id'],
|
|
'nom': row['nom'],
|
|
'ingredients': row['ingredients'],
|
|
'lien': row['lien']
|
|
}
|
|
|
|
# ✅ MODIFIÉ : Charger les accompagnements en utilisant la DATE + jour + repas
|
|
accompagnements = conn.execute('''
|
|
SELECT a.id, a.nom, a.descriptif
|
|
FROM menu_accompagnements ma
|
|
JOIN accompagnements a ON ma.accompagnement_id = a.id
|
|
WHERE ma.date = ? AND ma.jour = ? AND ma.repas = ?
|
|
ORDER BY a.nom
|
|
''', (row['date'], row['jour'], row['repas'])).fetchall()
|
|
|
|
menu[key]['accompagnements'] = [dict(acc) for acc in accompagnements]
|
|
|
|
# Calcul des dates pour l'affichage
|
|
days = []
|
|
jours = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
|
|
for i, jour in enumerate(jours):
|
|
date = start_of_week + timedelta(days=i)
|
|
days.append({
|
|
'name': jour,
|
|
'date': date.strftime('%d/%m/%Y')
|
|
})
|
|
|
|
# Récupération de toutes les recettes pour le dropdown
|
|
recettes = conn.execute('SELECT id, nom FROM recettes ORDER BY nom').fetchall()
|
|
|
|
# Récupération de tous les accompagnements pour le dropdown
|
|
tous_accompagnements = conn.execute('SELECT id, nom FROM accompagnements ORDER BY nom').fetchall()
|
|
|
|
start_date = start_of_week.strftime('%d/%m/%Y')
|
|
end_date = end_of_week.strftime('%d/%m/%Y')
|
|
|
|
conn.close()
|
|
|
|
return render_template('menu.html',
|
|
menu=menu,
|
|
days=days,
|
|
recettes=recettes,
|
|
tous_accompagnements=tous_accompagnements,
|
|
offset=offset,
|
|
start_date=start_date,
|
|
end_date=end_date)
|
|
|
|
|
|
@app.route('/add_to_menu', methods=['POST'])
|
|
@login_required
|
|
def add_to_menu():
|
|
day = request.form['day']
|
|
meal = request.form['meal']
|
|
recette_id = request.form['recette_id']
|
|
offset = int(request.form.get('offset', 0))
|
|
anchor = request.form.get('anchor', '') # ✅ AJOUTÉ
|
|
|
|
today = datetime.now().date()
|
|
start_of_week = today - timedelta(days=today.weekday()) + timedelta(weeks=offset)
|
|
jours_fr = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
|
|
day_index = jours_fr.index(day)
|
|
target_date = start_of_week + timedelta(days=day_index)
|
|
|
|
conn = get_connection()
|
|
|
|
# Supprimer l'ancienne entrée avant d'ajouter la nouvelle
|
|
conn.execute('''
|
|
DELETE FROM menu
|
|
WHERE date = ? AND jour = ? AND repas = ?
|
|
''', (target_date.isoformat(), day, meal))
|
|
|
|
# Ajouter la nouvelle recette
|
|
conn.execute('''
|
|
INSERT INTO menu (date, jour, repas, recette_id)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (target_date.isoformat(), day, meal, recette_id))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return redirect(url_for('menu', offset=offset, _anchor=anchor)) # ✅ MODIFIÉ
|
|
|
|
@app.route('/remove_from_menu', methods=['POST'])
|
|
@login_required
|
|
def remove_from_menu():
|
|
day = request.form['day']
|
|
meal = request.form['meal']
|
|
offset = int(request.form.get('offset', 0))
|
|
anchor = request.form.get('anchor', '') # ✅ AJOUTÉ
|
|
|
|
# Calculer la date exacte
|
|
today = datetime.now().date()
|
|
start_of_week = today - timedelta(days=today.weekday()) + timedelta(weeks=offset)
|
|
jours_fr = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
|
|
day_index = jours_fr.index(day)
|
|
target_date = start_of_week + timedelta(days=day_index)
|
|
|
|
conn = get_connection()
|
|
|
|
# Supprimer la recette du menu
|
|
conn.execute('''
|
|
DELETE FROM menu
|
|
WHERE date = ? AND jour = ? AND repas = ?
|
|
''', (target_date.isoformat(), day, meal))
|
|
|
|
# ✅ MODIFIÉ : Supprimer aussi les accompagnements avec la DATE
|
|
conn.execute('''
|
|
DELETE FROM menu_accompagnements
|
|
WHERE date = ? AND jour = ? AND repas = ?
|
|
''', (target_date.isoformat(), day, meal))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return redirect(url_for('menu', offset=offset, _anchor=anchor)) # ✅ MODIFIÉ
|
|
|
|
# ✅ MODIFIÉ : Ajouter un accompagnement au menu avec la DATE
|
|
@app.route('/add_accompagnement_to_menu', methods=['POST'])
|
|
@login_required
|
|
def add_accompagnement_to_menu():
|
|
day = request.form['day']
|
|
meal = request.form['meal']
|
|
accompagnement_id = request.form['accompagnement_id']
|
|
offset = int(request.form.get('offset', 0))
|
|
anchor = request.form.get('anchor', '') # ✅ AJOUTÉ
|
|
|
|
# Calculer la date exacte
|
|
today = datetime.now().date()
|
|
start_of_week = today - timedelta(days=today.weekday()) + timedelta(weeks=offset)
|
|
jours_fr = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
|
|
day_index = jours_fr.index(day)
|
|
target_date = start_of_week + timedelta(days=day_index)
|
|
|
|
conn = get_connection()
|
|
|
|
# Ajouter l'accompagnement pour CE jour/repas/date spécifique
|
|
conn.execute('''
|
|
INSERT OR IGNORE INTO menu_accompagnements (date, jour, repas, accompagnement_id)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (target_date.isoformat(), day, meal, accompagnement_id))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return redirect(url_for('menu', offset=offset, _anchor=anchor)) # ✅ MODIFIÉ
|
|
|
|
# ✅ MODIFIÉ : Retirer un accompagnement du menu avec la DATE
|
|
@app.route('/remove_accompagnement_from_menu', methods=['POST'])
|
|
@login_required
|
|
def remove_accompagnement_from_menu():
|
|
day = request.form['day']
|
|
meal = request.form['meal']
|
|
accompagnement_id = request.form['accompagnement_id']
|
|
offset = int(request.form.get('offset', 0))
|
|
anchor = request.form.get('anchor', '') # ✅ AJOUTÉ
|
|
|
|
# Calculer la date exacte
|
|
today = datetime.now().date()
|
|
start_of_week = today - timedelta(days=today.weekday()) + timedelta(weeks=offset)
|
|
jours_fr = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
|
|
day_index = jours_fr.index(day)
|
|
target_date = start_of_week + timedelta(days=day_index)
|
|
|
|
conn = get_connection()
|
|
|
|
# Retirer l'accompagnement pour CE jour/repas/date spécifique
|
|
conn.execute('''
|
|
DELETE FROM menu_accompagnements
|
|
WHERE date = ? AND jour = ? AND repas = ? AND accompagnement_id = ?
|
|
''', (target_date.isoformat(), day, meal, accompagnement_id))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return redirect(url_for('menu', offset=offset, _anchor=anchor)) # ✅ MODIFIÉ
|
|
|
|
@app.route('/change_week', methods=['POST'])
|
|
@login_required
|
|
def change_week():
|
|
offset = int(request.form['offset'])
|
|
return redirect(url_for('menu', offset=offset))
|
|
|
|
# ========== ✅ NOUVEAU : ROUTE STATISTIQUES ==========
|
|
@app.route('/stats')
|
|
@login_required
|
|
def stats():
|
|
conn = get_connection()
|
|
|
|
# 📊 Top 10 recettes les plus utilisées
|
|
top_recettes = conn.execute('''
|
|
SELECT r.nom, COUNT(*) as count
|
|
FROM menu m
|
|
JOIN recettes r ON m.recette_id = r.id
|
|
GROUP BY r.nom
|
|
ORDER BY count DESC
|
|
LIMIT 10
|
|
''').fetchall()
|
|
|
|
# 📊 Nombre total de recettes planifiées
|
|
total_planifiees = conn.execute('SELECT COUNT(*) as count FROM menu').fetchone()['count']
|
|
|
|
# 📊 Nombre total de recettes disponibles
|
|
total_recettes = conn.execute('SELECT COUNT(*) as count FROM recettes').fetchone()['count']
|
|
|
|
# 📊 Recettes jamais utilisées
|
|
jamais_utilisees = conn.execute('''
|
|
SELECT r.nom
|
|
FROM recettes r
|
|
LEFT JOIN menu m ON r.id = m.recette_id
|
|
WHERE m.id IS NULL
|
|
ORDER BY r.nom
|
|
''').fetchall()
|
|
|
|
# 📊 Accompagnements les plus utilisés
|
|
top_accompagnements = conn.execute('''
|
|
SELECT a.nom, COUNT(*) as count
|
|
FROM menu_accompagnements ma
|
|
JOIN accompagnements a ON ma.accompagnement_id = a.id
|
|
GROUP BY a.nom
|
|
ORDER BY count DESC
|
|
LIMIT 10
|
|
''').fetchall()
|
|
|
|
conn.close()
|
|
|
|
return render_template('stats.html',
|
|
top_recettes=top_recettes,
|
|
total_planifiees=total_planifiees,
|
|
total_recettes=total_recettes,
|
|
jamais_utilisees=jamais_utilisees,
|
|
top_accompagnements=top_accompagnements)
|
|
# ========== FIN NOUVEAU ==========
|
|
|
|
@app.route('/recettes')
|
|
@login_required
|
|
def recettes():
|
|
conn = get_connection()
|
|
|
|
search = request.args.get('search', '').strip()
|
|
|
|
if search:
|
|
recettes = conn.execute('''
|
|
SELECT * FROM recettes
|
|
WHERE nom LIKE ? OR ingredients LIKE ?
|
|
ORDER BY nom
|
|
''', (f'%{search}%', f'%{search}%')).fetchall()
|
|
else:
|
|
recettes = conn.execute('SELECT * FROM recettes ORDER BY nom').fetchall()
|
|
|
|
conn.close()
|
|
return render_template('recettes.html', recettes=recettes, search=search)
|
|
|
|
@app.route('/recettes/add', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_recette():
|
|
if request.method == 'POST':
|
|
nom = request.form['nom']
|
|
ingredients = request.form.get('ingredients', '')
|
|
instructions = request.form.get('instructions', '')
|
|
lien = request.form.get('lien', '')
|
|
|
|
conn = get_connection()
|
|
conn.execute('''
|
|
INSERT INTO recettes (nom, ingredients, instructions, lien)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (nom, ingredients, instructions, lien))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return redirect(url_for('recettes'))
|
|
|
|
return render_template('add_recette.html')
|
|
|
|
@app.route('/recettes/edit/<int:id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_recette(id):
|
|
conn = get_connection()
|
|
|
|
if request.method == 'POST':
|
|
nom = request.form['nom']
|
|
ingredients = request.form.get('ingredients', '')
|
|
instructions = request.form.get('instructions', '')
|
|
lien = request.form.get('lien', '')
|
|
|
|
# Mise à jour de la recette
|
|
conn.execute('''
|
|
UPDATE recettes
|
|
SET nom = ?, ingredients = ?, instructions = ?, lien = ?
|
|
WHERE id = ?
|
|
''', (nom, ingredients, instructions, lien, id))
|
|
|
|
# Gestion des accompagnements (suggestions par défaut pour cette recette)
|
|
# 1. Supprimer les anciennes associations
|
|
conn.execute('DELETE FROM recette_accompagnements WHERE recette_id = ?', (id,))
|
|
|
|
# 2. Ajouter les nouvelles associations
|
|
accompagnements_ids = request.form.getlist('accompagnements')
|
|
for acc_id in accompagnements_ids:
|
|
conn.execute('''
|
|
INSERT INTO recette_accompagnements (recette_id, accompagnement_id)
|
|
VALUES (?, ?)
|
|
''', (id, acc_id))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
flash('✅ Recette modifiée avec succès !', 'success')
|
|
return redirect(url_for('recettes'))
|
|
|
|
# Récupération des données pour l'affichage
|
|
recette = conn.execute('SELECT * FROM recettes WHERE id = ?', (id,)).fetchone()
|
|
accompagnements = conn.execute('SELECT * FROM accompagnements ORDER BY nom').fetchall()
|
|
|
|
# Récupérer les accompagnements sélectionnés
|
|
selected = conn.execute('''
|
|
SELECT accompagnement_id FROM recette_accompagnements
|
|
WHERE recette_id = ?
|
|
''', (id,)).fetchall()
|
|
selected_accompagnements = [row['accompagnement_id'] for row in selected]
|
|
|
|
conn.close()
|
|
|
|
return render_template('edit_recette.html',
|
|
recette=recette,
|
|
accompagnements=accompagnements,
|
|
selected_accompagnements=selected_accompagnements)
|
|
|
|
|
|
@app.route('/recettes/delete/<int:id>', methods=['POST'])
|
|
@login_required
|
|
def delete_recette(id):
|
|
conn = get_connection()
|
|
conn.execute('DELETE FROM recettes WHERE id = ?', (id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return redirect(url_for('recettes'))
|
|
|
|
@app.route('/export_recettes')
|
|
@login_required
|
|
def export_recettes():
|
|
"""Exporter toutes les recettes en JSON"""
|
|
conn = get_connection()
|
|
recettes = conn.execute('SELECT nom, ingredients, instructions, lien FROM recettes ORDER BY nom').fetchall()
|
|
conn.close()
|
|
|
|
recettes_list = [dict(row) for row in recettes]
|
|
json_data = json.dumps(recettes_list, ensure_ascii=False, indent=2)
|
|
|
|
buffer = BytesIO()
|
|
buffer.write(json_data.encode('utf-8'))
|
|
buffer.seek(0)
|
|
|
|
filename = f"recettes_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
|
|
return send_file(
|
|
buffer,
|
|
mimetype='application/json',
|
|
as_attachment=True,
|
|
download_name=filename
|
|
)
|
|
|
|
@app.route('/import_recettes', methods=['POST'])
|
|
@login_required
|
|
def import_recettes():
|
|
"""Importer des recettes depuis un fichier JSON"""
|
|
if 'file' not in request.files:
|
|
flash('Aucun fichier sélectionné', 'error')
|
|
return redirect(url_for('recettes'))
|
|
|
|
file = request.files['file']
|
|
|
|
if file.filename == '':
|
|
flash('Aucun fichier sélectionné', 'error')
|
|
return redirect(url_for('recettes'))
|
|
|
|
if not file.filename.endswith('.json'):
|
|
flash('Le fichier doit être au format JSON', 'error')
|
|
return redirect(url_for('recettes'))
|
|
|
|
try:
|
|
data = json.load(file)
|
|
|
|
if not isinstance(data, list):
|
|
flash('Format JSON invalide (doit être une liste)', 'error')
|
|
return redirect(url_for('recettes'))
|
|
|
|
conn = get_connection()
|
|
imported = 0
|
|
updated = 0
|
|
|
|
for recette in data:
|
|
nom = recette.get('nom', '').strip()
|
|
if not nom:
|
|
continue
|
|
|
|
ingredients = recette.get('ingredients', '')
|
|
instructions = recette.get('instructions', '')
|
|
lien = recette.get('lien', '')
|
|
|
|
existing = conn.execute('SELECT id FROM recettes WHERE nom = ?', (nom,)).fetchone()
|
|
|
|
if existing:
|
|
conn.execute('''
|
|
UPDATE recettes
|
|
SET ingredients=?, instructions=?, lien=?
|
|
WHERE nom=?
|
|
''', (ingredients, instructions, lien, nom))
|
|
updated += 1
|
|
else:
|
|
conn.execute('''
|
|
INSERT INTO recettes (nom, ingredients, instructions, lien)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (nom, ingredients, instructions, lien))
|
|
imported += 1
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
flash(f'✅ Import réussi ! {imported} recette(s) ajoutée(s), {updated} mise(s) à jour', 'success')
|
|
|
|
except json.JSONDecodeError:
|
|
flash('❌ Erreur : fichier JSON invalide', 'error')
|
|
except Exception as e:
|
|
flash(f'❌ Erreur lors de l\'import : {str(e)}', 'error')
|
|
|
|
return redirect(url_for('recettes'))
|
|
|
|
# ========== ROUTES ACCOMPAGNEMENTS ==========
|
|
@app.route('/accompagnements')
|
|
@login_required
|
|
def accompagnements():
|
|
conn = get_connection()
|
|
accompagnements = conn.execute('SELECT * FROM accompagnements ORDER BY nom').fetchall()
|
|
conn.close()
|
|
return render_template('accompagnements.html', accompagnements=accompagnements)
|
|
|
|
@app.route('/accompagnements/add', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_accompagnement():
|
|
if request.method == 'POST':
|
|
nom = request.form['nom'].strip()
|
|
descriptif = request.form.get('descriptif', '').strip()
|
|
|
|
conn = get_connection()
|
|
conn.execute('''
|
|
INSERT INTO accompagnements (nom, descriptif)
|
|
VALUES (?, ?)
|
|
''', (nom, descriptif))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
flash('✅ Accompagnement ajouté avec succès !', 'success')
|
|
return redirect(url_for('accompagnements'))
|
|
|
|
return render_template('add_accompagnement.html')
|
|
|
|
@app.route('/accompagnements/edit/<int:id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_accompagnement(id):
|
|
conn = get_connection()
|
|
|
|
if request.method == 'POST':
|
|
nom = request.form['nom'].strip()
|
|
descriptif = request.form.get('descriptif', '').strip()
|
|
|
|
conn.execute('''
|
|
UPDATE accompagnements
|
|
SET nom = ?, descriptif = ?
|
|
WHERE id = ?
|
|
''', (nom, descriptif, id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
flash('✅ Accompagnement modifié avec succès !', 'success')
|
|
return redirect(url_for('accompagnements'))
|
|
|
|
accompagnement = conn.execute('SELECT * FROM accompagnements WHERE id = ?', (id,)).fetchone()
|
|
conn.close()
|
|
|
|
return render_template('edit_accompagnement.html', accompagnement=accompagnement)
|
|
|
|
@app.route('/accompagnements/delete/<int:id>', methods=['POST'])
|
|
@login_required
|
|
def delete_accompagnement(id):
|
|
conn = get_connection()
|
|
conn.execute('DELETE FROM accompagnements WHERE id = ?', (id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
flash('✅ Accompagnement supprimé avec succès !', 'success')
|
|
return redirect(url_for('accompagnements'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=False, host='0.0.0.0')
|