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>
This commit is contained in:
2026-01-20 03:30:09 +01:00
parent 6a10d496d8
commit 58e9aa1429
5 changed files with 83 additions and 1 deletions

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import json
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
@@ -57,9 +59,21 @@ def create_product_with_snapshot(
"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"])
@@ -161,6 +175,28 @@ def _enrich_product_with_snapshot(db: Session, product: models.Product) -> dict:
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,
@@ -175,6 +211,10 @@ def _enrich_product_with_snapshot(db: Session, product: models.Product) -> dict:
"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,
}

View File

@@ -62,6 +62,11 @@ class ProductSnapshot(Base):
choix_amazon = Column(Boolean, nullable=True)
offre_limitee = Column(Boolean, nullable=True)
exclusivite_amazon = Column(Boolean, nullable=True)
# Données étendues (stockées en JSON)
a_propos = Column(Text, nullable=True) # JSON array
description = Column(Text, nullable=True)
carateristique = Column(Text, nullable=True) # JSON object
details = Column(Text, nullable=True) # JSON object
chemin_json_brut = Column(Text, nullable=True)
statut_scrap = Column(String(32), default="ok")
message_erreur = Column(Text, nullable=True)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Optional
from typing import Any, Optional
from pydantic import BaseModel, HttpUrl
@@ -50,6 +50,11 @@ class ProductSnapshotBase(BaseModel):
choix_amazon: Optional[bool]
offre_limitee: Optional[bool]
exclusivite_amazon: Optional[bool]
# Données étendues
a_propos: Optional[list[str]] = None
description: Optional[str] = None
carateristique: Optional[dict[str, Any]] = None
details: Optional[dict[str, Any]] = None
statut_scrap: Optional[str]
message_erreur: Optional[str]
@@ -82,6 +87,11 @@ class ProductWithSnapshot(ProductBase):
choix_amazon: Optional[bool] = None
offre_limitee: Optional[bool] = None
exclusivite_amazon: Optional[bool] = None
# Données étendues
a_propos: Optional[list[str]] = None
description: Optional[str] = None
carateristique: Optional[dict[str, Any]] = None
details: Optional[dict[str, Any]] = None
dernier_scrape: Optional[datetime] = None
statut_scrap: Optional[str] = None
@@ -104,3 +114,8 @@ class ProductCreateWithSnapshot(ProductBase):
choix_amazon: Optional[bool] = None
offre_limitee: Optional[bool] = None
exclusivite_amazon: Optional[bool] = None
# Données étendues
a_propos: Optional[list[str]] = None
description: Optional[str] = None
carateristique: Optional[dict[str, Any]] = None
details: Optional[dict[str, Any]] = None

View File

@@ -93,6 +93,19 @@ def _create_snapshot(
# Mettre à jour le produit avec titre/image si manquants
_update_product_from_scrape(session, product, data)
# Sérialiser les données étendues en JSON
a_propos = data.get("a_propos")
if a_propos is not None:
a_propos = json.dumps(a_propos, ensure_ascii=False)
carateristique = data.get("carateristique")
if carateristique is not None:
carateristique = json.dumps(carateristique, ensure_ascii=False)
details = data.get("details")
if details is not None:
details = json.dumps(details, ensure_ascii=False)
snapshot = models.ProductSnapshot(
produit_id=product.id,
run_scrap_id=run.id,
@@ -107,6 +120,10 @@ def _create_snapshot(
choix_amazon=data.get("choix_amazon"),
offre_limitee=data.get("offre_limitee"),
exclusivite_amazon=data.get("exclusivite_amazon"),
a_propos=a_propos,
description=data.get("description"),
carateristique=carateristique,
details=details,
chemin_json_brut=str(raw_json_path) if raw_json_path else None,
statut_scrap=status,
message_erreur=error_message,

View File

@@ -100,6 +100,11 @@ const AddProductModal = ({ onClose }) => {
choix_amazon: data.choix_amazon ?? null,
offre_limitee: data.offre_limitee ?? null,
exclusivite_amazon: data.exclusivite_amazon ?? null,
// Données étendues
a_propos: data.a_propos ?? null,
description: data.description ?? null,
carateristique: data.carateristique ?? null,
details: data.details ?? null,
});
onClose();