Files
suivi_produit/backend/app/db/crud.py
gilles 58e9aa1429 feat: stocker et afficher les données étendues dans le modal détail
- Ajout des champs a_propos, description, carateristique, details au modèle ProductSnapshot
- Sérialisation JSON pour les listes et dictionnaires
- Modification du CRUD pour stocker/lire les données étendues
- Modification du runner pour passer les données lors du scrape
- AddProductModal envoie les données étendues lors de la création
- La base SQLite doit être recréée (suppression de suivi.db)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 03:30:09 +01:00

224 lines
7.1 KiB
Python

from __future__ import annotations
import json
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from backend.app.db import models, schemas
"""
CRUD minimal pour manipuler les produits dans la base SQLite.
"""
def get_product(db: Session, product_id: int) -> models.Product | None:
return db.query(models.Product).filter(models.Product.id == product_id).first()
def list_products(db: Session, skip: int = 0, limit: int = 100) -> list[models.Product]:
return db.query(models.Product).offset(skip).limit(limit).all()
def create_product(db: Session, data: schemas.ProductCreate) -> models.Product:
# Convertir les HttpUrl en strings pour SQLite
data_dict = data.model_dump()
if data_dict.get("url"):
data_dict["url"] = str(data_dict["url"])
if data_dict.get("url_image"):
data_dict["url_image"] = str(data_dict["url_image"])
product = models.Product(**data_dict)
db.add(product)
try:
db.commit()
except IntegrityError:
db.rollback()
raise
db.refresh(product)
return product
def create_product_with_snapshot(
db: Session, data: schemas.ProductCreateWithSnapshot
) -> models.Product:
"""Crée un produit avec un snapshot initial (données issues de la prévisualisation)."""
data_dict = data.model_dump()
# Extraire les champs du snapshot
snapshot_fields = [
"prix_actuel",
"prix_conseille",
"prix_min_30j",
"etat_stock",
"en_stock",
"note",
"nombre_avis",
"prime",
"choix_amazon",
"offre_limitee",
"exclusivite_amazon",
"a_propos",
"description",
"carateristique",
"details",
]
snapshot_data = {k: data_dict.pop(k) for k in snapshot_fields if k in data_dict}
# Sérialiser les champs JSON pour le stockage
if snapshot_data.get("a_propos") is not None:
snapshot_data["a_propos"] = json.dumps(snapshot_data["a_propos"], ensure_ascii=False)
if snapshot_data.get("carateristique") is not None:
snapshot_data["carateristique"] = json.dumps(snapshot_data["carateristique"], ensure_ascii=False)
if snapshot_data.get("details") is not None:
snapshot_data["details"] = json.dumps(snapshot_data["details"], ensure_ascii=False)
# Convertir les HttpUrl en strings pour SQLite
if data_dict.get("url"):
data_dict["url"] = str(data_dict["url"])
if data_dict.get("url_image"):
data_dict["url_image"] = str(data_dict["url_image"])
# Créer le produit
product = models.Product(**data_dict)
db.add(product)
try:
db.commit()
except IntegrityError:
db.rollback()
raise
db.refresh(product)
# Créer le snapshot initial si on a des données
has_snapshot_data = any(v is not None for v in snapshot_data.values())
if has_snapshot_data:
snapshot = models.ProductSnapshot(
produit_id=product.id,
statut_scrap="preview",
**snapshot_data,
)
db.add(snapshot)
db.commit()
return product
def update_product(db: Session, product: models.Product, changes: schemas.ProductUpdate) -> models.Product:
for field, value in changes.dict(exclude_unset=True).items():
setattr(product, field, value)
db.add(product)
db.commit()
db.refresh(product)
return product
def remove_product(db: Session, product: models.Product) -> None:
db.delete(product)
db.commit()
def list_snapshots(db: Session, product_id: int, limit: int = 30) -> list[models.ProductSnapshot]:
return (
db.query(models.ProductSnapshot)
.filter(models.ProductSnapshot.produit_id == product_id)
.order_by(models.ProductSnapshot.scrape_le.desc())
.limit(limit)
.all()
)
def get_latest_snapshot(db: Session, product_id: int) -> models.ProductSnapshot | None:
return (
db.query(models.ProductSnapshot)
.filter(models.ProductSnapshot.produit_id == product_id)
.order_by(models.ProductSnapshot.scrape_le.desc())
.first()
)
def get_product_with_snapshot(db: Session, product_id: int) -> dict | None:
"""Retourne un produit enrichi avec les données du dernier snapshot."""
product = get_product(db, product_id)
if not product:
return None
return _enrich_product_with_snapshot(db, product)
def list_products_with_snapshots(db: Session, skip: int = 0, limit: int = 100) -> list[dict]:
"""Retourne la liste des produits enrichis avec leurs derniers snapshots."""
products = list_products(db, skip=skip, limit=limit)
return [_enrich_product_with_snapshot(db, p) for p in products]
def _enrich_product_with_snapshot(db: Session, product: models.Product) -> dict:
"""Ajoute les données du dernier snapshot au produit."""
snapshot = get_latest_snapshot(db, product.id)
result = {
"id": product.id,
"boutique": product.boutique,
"url": str(product.url),
"asin": product.asin,
"titre": product.titre,
"url_image": str(product.url_image) if product.url_image else None,
"categorie": product.categorie,
"type": product.type,
"actif": product.actif,
"cree_le": product.cree_le,
"modifie_le": product.modifie_le,
}
if snapshot:
# Calcul de la réduction en pourcentage
reduction = None
if snapshot.prix_actuel and snapshot.prix_conseille:
reduction = round((1 - snapshot.prix_actuel / snapshot.prix_conseille) * 100)
# Désérialiser les champs JSON
a_propos = None
if snapshot.a_propos:
try:
a_propos = json.loads(snapshot.a_propos)
except json.JSONDecodeError:
a_propos = None
carateristique = None
if snapshot.carateristique:
try:
carateristique = json.loads(snapshot.carateristique)
except json.JSONDecodeError:
carateristique = None
details = None
if snapshot.details:
try:
details = json.loads(snapshot.details)
except json.JSONDecodeError:
details = None
result.update(
{
"prix_actuel": snapshot.prix_actuel,
"prix_conseille": snapshot.prix_conseille,
"prix_min_30j": snapshot.prix_min_30j,
"reduction_pourcent": reduction,
"etat_stock": snapshot.etat_stock,
"en_stock": snapshot.en_stock,
"note": snapshot.note,
"nombre_avis": snapshot.nombre_avis,
"prime": snapshot.prime,
"choix_amazon": snapshot.choix_amazon,
"offre_limitee": snapshot.offre_limitee,
"exclusivite_amazon": snapshot.exclusivite_amazon,
"a_propos": a_propos,
"description": snapshot.description,
"carateristique": carateristique,
"details": details,
"dernier_scrape": snapshot.scrape_le,
"statut_scrap": snapshot.statut_scrap,
}
)
return result