Ajoute les thèmes communs sur les fiches de livres
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
e28d5bce32
commit
c2d408afc6
@@ -2008,3 +2008,28 @@ la position la plus précoce et un rôle renseigné plutôt que `NonPrecise`, pu
|
||||
ses refus deviennent obsolètes. Sans collision, le renommage est direct. Cette opération est
|
||||
volontairement distincte de la renormalisation automatique : c'est un choix explicite de
|
||||
l'utilisateur.
|
||||
|
||||
### F2 : thèmes en étiquettes (2026-08-20)
|
||||
|
||||
Les thèmes décrivent l'œuvre (« dark fantasy », « space opera »), pas la relation personnelle
|
||||
d'un lecteur à un livre. Ils sont donc **communs au foyer**, comme le catalogue, les auteurs et
|
||||
les séries : `AjoutePar` reste une trace de saisie et ne crée aucune frontière. Les statuts de
|
||||
lecture et les envies restent les seules données personnelles parce qu'elles expriment une
|
||||
relation de l'utilisateur, et non une propriété de l'œuvre.
|
||||
|
||||
La saisie est **manuelle uniquement**. La BnF expose bien `dc:subject`, mais son vocabulaire
|
||||
Rameau est une indexation documentaire professionnelle et ne correspond pas aux étiquettes
|
||||
attendues par l'utilisateur. Aucun thème n'est donc déduit automatiquement d'un lookup ISBN ;
|
||||
comme pour les rôles d'auteurs, l'utilisateur choisit ce qu'il veut réellement afficher.
|
||||
|
||||
Le modèle est une table dédiée `Theme` (`Nom`, `NomNormalise`) et une table n-n `LivreTheme`,
|
||||
avec clé composite et suppression en cascade depuis le livre. La forme normalisée réutilise
|
||||
`NormalisationTexte` et porte un index unique : « Fantasy » et « fantasy » partagent la même
|
||||
fiche. Le formulaire accepte une liste séparée par des virgules, dédoublonnée avant résolution ;
|
||||
le premier libellé saisi devient la forme affichée. Les thèmes apparaissent en étiquettes sur la
|
||||
carte partagée `CarteLivre` et sur la fiche en consultation.
|
||||
|
||||
Le filtre par thème n'est **pas inclus dans F2**. Il aurait fallu ajouter une dimension aux deux
|
||||
implémentations de filtrage (`FiltreLivres` et `FiltreLivresLocal`), au contrat de cache et aux
|
||||
tests de confrontation ; cette extension est reportée à un lot ultérieur plutôt que d'introduire
|
||||
un filtre partiel ou divergent.
|
||||
|
||||
@@ -239,10 +239,12 @@ L'écran `/souhaits/ajout` rend aujourd'hui une liste plate.
|
||||
recours quand une donnée paraît fausse. ⚠️ Suppose de **conserver l'identifiant de la notice**
|
||||
au lookup, ce que le modèle ne fait pas aujourd'hui : ce n'est donc pas un simple lien à
|
||||
afficher, mais une colonne de plus.
|
||||
- **F2. Des thèmes en étiquettes** (dark fantasy, science-fiction, space opera…). Grosse maille :
|
||||
table de tags, saisie, filtre, et la question de la **portée** (commune ? personnelle ?). Une
|
||||
source automatique est douteuse — le Dublin Core BnF porte `dc:subject`, mais avec un vocabulaire
|
||||
Rameau qui ne ressemble pas à ces mots-là. À instruire séparément du reste du lot.
|
||||
- ~~**F2. Des thèmes en étiquettes** (dark fantasy, science-fiction, space opera…).~~ Traité le
|
||||
2026-08-20 : thèmes communs au foyer dans `Theme`/`LivreTheme`, saisis manuellement et
|
||||
normalisés pour éviter les doublons. La BnF n'alimente pas ces étiquettes : son `dc:subject`
|
||||
est en vocabulaire Rameau, inadapté aux mots-clés grand public. Le filtre catalogue est
|
||||
volontairement reporté ; le formulaire accepte une liste séparée par des virgules et les
|
||||
thèmes s'affichent sur les cartes et fiches.
|
||||
- ~~**F3. Renommer un auteur**, la correction se répercutant sur tous ses livres.~~ Traité le
|
||||
2026-08-20 : le renommage recalcule les formes et propose une fusion confirmable en cas de
|
||||
collision. ⚠️ Le n-n est déjà
|
||||
|
||||
@@ -11,6 +11,10 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
|
||||
public DbSet<LivreAuteur> LivreAuteurs => Set<LivreAuteur>();
|
||||
|
||||
public DbSet<Theme> Themes => Set<Theme>();
|
||||
|
||||
public DbSet<LivreTheme> LivreThemes => Set<LivreTheme>();
|
||||
|
||||
public DbSet<StatutLecture> StatutsLecture => Set<StatutLecture>();
|
||||
|
||||
public DbSet<RapprochementRefuse> RapprochementsRefuses => Set<RapprochementRefuse>();
|
||||
@@ -94,6 +98,30 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
lien.HasIndex(la => la.AuteurId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Theme>(theme =>
|
||||
{
|
||||
theme.Property(t => t.Nom).IsRequired();
|
||||
theme.Property(t => t.NomNormalise).IsRequired();
|
||||
theme.HasIndex(t => t.NomNormalise).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<LivreTheme>(lien =>
|
||||
{
|
||||
lien.HasKey(lt => new { lt.LivreId, lt.ThemeId });
|
||||
|
||||
lien.HasOne(lt => lt.Livre)
|
||||
.WithMany(l => l.Themes)
|
||||
.HasForeignKey(lt => lt.LivreId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
lien.HasOne(lt => lt.Theme)
|
||||
.WithMany(t => t.Livres)
|
||||
.HasForeignKey(lt => lt.ThemeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
lien.HasIndex(lt => lt.ThemeId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<StatutLecture>(statut =>
|
||||
{
|
||||
statut.Property(s => s.Utilisateur).IsRequired();
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MaBibli.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MaBibli.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(MaBibliDbContext))]
|
||||
[Migration("20260820123516_ThemesEtEtiquettes")]
|
||||
partial class ThemesEtEtiquettes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.11");
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Auteur", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CleRegroupement")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nom")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NomNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CleRegroupement")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("NomNormalise");
|
||||
|
||||
b.ToTable("Auteurs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.BibliographieMasquee", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AuteurId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TitreNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Utilisateur")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Utilisateur", "AuteurId");
|
||||
|
||||
b.HasIndex(new[] { "AuteurId", "Utilisateur", "TitreNormalise" }, "IX_BibliographiesMasquees_Auteur_Utilisateur_Titre")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BibliographiesMasquees");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.ElementSerie", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SerieId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Titre")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LivreId");
|
||||
|
||||
b.HasIndex(new[] { "SerieId" }, "IX_ElementsSerie_SerieId");
|
||||
|
||||
b.HasIndex(new[] { "SerieId", "LivreId" }, "IX_ElementsSerie_SerieId_LivreId")
|
||||
.IsUnique()
|
||||
.HasFilter("\"LivreId\" IS NOT NULL");
|
||||
|
||||
b.ToTable("ElementsSerie");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Livre", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AjoutePar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAjout")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Editeur")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Isbn")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Titre")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TitreNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TypeDocument")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UrlNotice")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Isbn");
|
||||
|
||||
b.HasIndex("TitreNormalise");
|
||||
|
||||
b.ToTable("Livres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreAuteur", b =>
|
||||
{
|
||||
b.Property<int>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AuteurId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LivreId", "AuteurId");
|
||||
|
||||
b.HasIndex("AuteurId");
|
||||
|
||||
b.ToTable("LivreAuteurs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreSouhaite", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Annee")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Auteur")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuteurNormalise")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAjout")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Editeur")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Isbn")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Rang")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Titre")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TitreNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Utilisateur")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "Utilisateur" }, "IX_LivresSouhaites_Utilisateur");
|
||||
|
||||
b.HasIndex(new[] { "Utilisateur", "TitreNormalise", "AuteurNormalise" }, "IX_LivresSouhaites_Utilisateur_Oeuvre")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LivresSouhaites");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreTheme", b =>
|
||||
{
|
||||
b.Property<int>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ThemeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LivreId", "ThemeId");
|
||||
|
||||
b.HasIndex("ThemeId");
|
||||
|
||||
b.ToTable("LivreThemes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAjout")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateParution")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Numero")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NumeroNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RevueId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RevueId", "NumeroNormalise")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("NumerosRevue");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Pret", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DatePret")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateRetour")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Emprunteur")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "LivreId" }, "IX_Prets_LivreId");
|
||||
|
||||
b.HasIndex(new[] { "LivreId" }, "IX_Prets_LivreId_EnCours")
|
||||
.IsUnique()
|
||||
.HasFilter("\"DateRetour\" IS NULL");
|
||||
|
||||
b.ToTable("Prets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.RapprochementRefuse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AuteurAId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AuteurBId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuteurAId", "AuteurBId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RapprochementsRefuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AjoutePar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAjout")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Editeur")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Issn")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Titre")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TitreNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TitreNormalise")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex(new[] { "Issn" }, "IX_Revues_Issn")
|
||||
.IsUnique()
|
||||
.HasFilter("\"Issn\" IS NOT NULL");
|
||||
|
||||
b.ToTable("Revues");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Serie", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AjoutePar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAjout")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SerieParenteId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Titre")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TitreNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SerieParenteId");
|
||||
|
||||
b.HasIndex("TitreNormalise")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.StatutLecture", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateMaj")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Statut")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Utilisateur")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Utilisateur");
|
||||
|
||||
b.HasIndex("LivreId", "Utilisateur")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StatutsLecture");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Theme", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Nom")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NomNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NomNormalise")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Themes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.ElementSerie", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany()
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("MaBibli.Shared.Entites.Serie", "Serie")
|
||||
.WithMany("Elements")
|
||||
.HasForeignKey("SerieId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Livre");
|
||||
|
||||
b.Navigation("Serie");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreAuteur", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Auteur", "Auteur")
|
||||
.WithMany("Livres")
|
||||
.HasForeignKey("AuteurId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany("Auteurs")
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Auteur");
|
||||
|
||||
b.Navigation("Livre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreTheme", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany("Themes")
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MaBibli.Shared.Entites.Theme", "Theme")
|
||||
.WithMany("Livres")
|
||||
.HasForeignKey("ThemeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Livre");
|
||||
|
||||
b.Navigation("Theme");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Revue", "Revue")
|
||||
.WithMany("Numeros")
|
||||
.HasForeignKey("RevueId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Revue");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Pret", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany("Prets")
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Livre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Serie", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Serie", "SerieParente")
|
||||
.WithMany("SousSeries")
|
||||
.HasForeignKey("SerieParenteId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SerieParente");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.StatutLecture", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany("Statuts")
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Livre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Auteur", b =>
|
||||
{
|
||||
b.Navigation("Livres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Livre", b =>
|
||||
{
|
||||
b.Navigation("Auteurs");
|
||||
|
||||
b.Navigation("Prets");
|
||||
|
||||
b.Navigation("Statuts");
|
||||
|
||||
b.Navigation("Themes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
||||
{
|
||||
b.Navigation("Numeros");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Serie", b =>
|
||||
{
|
||||
b.Navigation("Elements");
|
||||
|
||||
b.Navigation("SousSeries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Theme", b =>
|
||||
{
|
||||
b.Navigation("Livres");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MaBibli.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ThemesEtEtiquettes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Themes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Nom = table.Column<string>(type: "TEXT", nullable: false),
|
||||
NomNormalise = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Themes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LivreThemes",
|
||||
columns: table => new
|
||||
{
|
||||
LivreId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ThemeId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LivreThemes", x => new { x.LivreId, x.ThemeId });
|
||||
table.ForeignKey(
|
||||
name: "FK_LivreThemes_Livres_LivreId",
|
||||
column: x => x.LivreId,
|
||||
principalTable: "Livres",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_LivreThemes_Themes_ThemeId",
|
||||
column: x => x.ThemeId,
|
||||
principalTable: "Themes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LivreThemes_ThemeId",
|
||||
table: "LivreThemes",
|
||||
column: "ThemeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Themes_NomNormalise",
|
||||
table: "Themes",
|
||||
column: "NomNormalise",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LivreThemes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Themes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,21 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.ToTable("LivresSouhaites");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreTheme", b =>
|
||||
{
|
||||
b.Property<int>("LivreId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ThemeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LivreId", "ThemeId");
|
||||
|
||||
b.HasIndex("ThemeId");
|
||||
|
||||
b.ToTable("LivreThemes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -417,6 +432,28 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.ToTable("StatutsLecture");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Theme", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Nom")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NomNormalise")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NomNormalise")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Themes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.ElementSerie", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
@@ -454,6 +491,25 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.Navigation("Livre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.LivreTheme", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||
.WithMany("Themes")
|
||||
.HasForeignKey("LivreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MaBibli.Shared.Entites.Theme", "Theme")
|
||||
.WithMany("Livres")
|
||||
.HasForeignKey("ThemeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Livre");
|
||||
|
||||
b.Navigation("Theme");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||
{
|
||||
b.HasOne("MaBibli.Shared.Entites.Revue", "Revue")
|
||||
@@ -509,6 +565,8 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.Navigation("Prets");
|
||||
|
||||
b.Navigation("Statuts");
|
||||
|
||||
b.Navigation("Themes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
||||
@@ -522,6 +580,11 @@ namespace MaBibli.Api.Data.Migrations
|
||||
|
||||
b.Navigation("SousSeries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Theme", b =>
|
||||
{
|
||||
b.Navigation("Livres");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,14 @@ public sealed class ServiceRenormalisation(MaBibliDbContext db, ILogger<ServiceR
|
||||
n => n.RecalculerFormes(),
|
||||
n => $"le numéro {n.Numero}");
|
||||
|
||||
var themes = await db.Themes.OrderBy(t => t.Id).ToListAsync(ct);
|
||||
corriges += Renormaliser(
|
||||
themes,
|
||||
t => t.NomNormalise,
|
||||
t => NormalisationTexte.Normaliser(t.Nom),
|
||||
t => t.RecalculerFormes(),
|
||||
t => $"le thème « {t.Nom} »");
|
||||
|
||||
if (corriges > 0)
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@@ -72,7 +72,9 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
// AsNoTracking : lecture pure. Et surtout : AUCUN filtre sur AjoutePar,
|
||||
// la bibliothèque est commune à tout le foyer (CLAUDE.md).
|
||||
var requete = FiltreLivres.Appliquer(
|
||||
db.Livres.AsNoTracking().Include(l => l.Auteurs).ThenInclude(la => la.Auteur),
|
||||
db.Livres.AsNoTracking()
|
||||
.Include(l => l.Auteurs).ThenInclude(la => la.Auteur)
|
||||
.Include(l => l.Themes).ThenInclude(lt => lt.Theme),
|
||||
criteres,
|
||||
utilisateur);
|
||||
|
||||
@@ -92,6 +94,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
var livre = await db.Livres
|
||||
.AsNoTracking()
|
||||
.Include(l => l.Auteurs).ThenInclude(la => la.Auteur)
|
||||
.Include(l => l.Themes).ThenInclude(lt => lt.Theme)
|
||||
.FirstOrDefaultAsync(l => l.Id == id, ct);
|
||||
|
||||
if (livre is null)
|
||||
@@ -143,6 +146,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
db.Livres.Add(livre);
|
||||
|
||||
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
||||
await RattacherThemesAsync(livre, saisie.Themes, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// Le statut n'est écrit que si l'utilisateur en a explicitement posé un : un livre sans
|
||||
@@ -155,7 +159,10 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
public async Task<ResultatEcriture> ModifierAsync(
|
||||
int id, EnregistrementLivre saisie, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
var livre = await db.Livres.Include(l => l.Auteurs).FirstOrDefaultAsync(l => l.Id == id, ct);
|
||||
var livre = await db.Livres
|
||||
.Include(l => l.Auteurs)
|
||||
.Include(l => l.Themes).ThenInclude(lt => lt.Theme)
|
||||
.FirstOrDefaultAsync(l => l.Id == id, ct);
|
||||
if (livre is null)
|
||||
{
|
||||
return ResultatEcriture.Introuvable;
|
||||
@@ -179,6 +186,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
// d'origine, pas des champs éditables.
|
||||
|
||||
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
||||
await RattacherThemesAsync(livre, saisie.Themes, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// Le statut modifié est celui de la personne qui édite, pas de celle qui a saisi le livre.
|
||||
@@ -287,7 +295,9 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
}
|
||||
|
||||
private IQueryable<Livre> AvecAuteurs() =>
|
||||
db.Livres.AsNoTracking().Include(l => l.Auteurs).ThenInclude(la => la.Auteur);
|
||||
db.Livres.AsNoTracking()
|
||||
.Include(l => l.Auteurs).ThenInclude(la => la.Auteur)
|
||||
.Include(l => l.Themes).ThenInclude(lt => lt.Theme);
|
||||
|
||||
/// <summary>Un des auteurs du livre catalogué est-il l'un de ceux qu'on est en train de saisir ?</summary>
|
||||
/// <remarks>Le rôle n'entre pas dans la comparaison : c'est la personne qui compte ici.</remarks>
|
||||
@@ -444,6 +454,37 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RattacherThemesAsync(
|
||||
Livre livre, IReadOnlyList<string> saisis, CancellationToken ct)
|
||||
{
|
||||
var themes = saisis
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Select(t => t.Trim())
|
||||
.GroupBy(NormalisationTexte.Normaliser, StringComparer.Ordinal)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (livre.Themes.Count > 0)
|
||||
{
|
||||
db.LivreThemes.RemoveRange(livre.Themes);
|
||||
livre.Themes.Clear();
|
||||
}
|
||||
|
||||
foreach (var nom in themes)
|
||||
{
|
||||
var normalise = NormalisationTexte.Normaliser(nom);
|
||||
var theme = await db.Themes.FirstOrDefaultAsync(t => t.NomNormalise == normalise, ct);
|
||||
if (theme is null)
|
||||
{
|
||||
theme = new Theme { Nom = nom };
|
||||
theme.RecalculerFormes();
|
||||
db.Themes.Add(theme);
|
||||
}
|
||||
|
||||
livre.Themes.Add(new LivreTheme { Livre = livre, Theme = theme });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contrôles minimaux : un titre, et un ISBN qui tienne debout s'il est fourni.
|
||||
/// </summary>
|
||||
@@ -491,6 +532,11 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
.Where(la => la.Auteur is not null)
|
||||
.Select(la => new AuteurDto { Id = la.Auteur!.Id, Nom = la.Auteur.Nom, Role = la.Role })
|
||||
.ToList(),
|
||||
Themes = livre.Themes
|
||||
.Where(lt => lt.Theme is not null)
|
||||
.Select(lt => lt.Theme!.Nom)
|
||||
.OrderBy(nom => NormalisationTexte.Normaliser(nom), StringComparer.Ordinal)
|
||||
.ToList(),
|
||||
Editeur = livre.Editeur,
|
||||
Format = livre.Format,
|
||||
TypeDocument = livre.TypeDocument,
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
@Libelles.TypeDocument(Livre.TypeDocument)
|
||||
</span>
|
||||
}
|
||||
@foreach (var theme in Livre.Themes)
|
||||
{
|
||||
<span class="etiquette">@theme</span>
|
||||
}
|
||||
@if (Livre.Statut is { } statut)
|
||||
{
|
||||
<span class="etiquette etiquette-statut @Libelles.ClasseStatut(statut)">
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
autocomplete="off" />
|
||||
</label>
|
||||
|
||||
<label class="champ">
|
||||
<span class="champ-libelle">Thèmes <span class="champ-aide">(séparés par des virgules)</span></span>
|
||||
<input class="champ-saisie" type="text" @bind="Saisie.ThemesTexte" @bind:event="oninput"
|
||||
placeholder="Fantasy, aventure, space opera" autocomplete="off" />
|
||||
</label>
|
||||
|
||||
<label class="champ">
|
||||
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
|
||||
<input class="champ-saisie" type="text" inputmode="numeric" @bind="Saisie.Isbn" @bind:event="oninput"
|
||||
|
||||
@@ -150,6 +150,19 @@ else
|
||||
<div><dt>Type</dt><dd>@Libelles.TypeDocument(_livre.TypeDocument)</dd></div>
|
||||
}
|
||||
|
||||
@if (_livre.Themes.Count > 0)
|
||||
{
|
||||
<div>
|
||||
<dt>Thèmes</dt>
|
||||
<dd class="etiquettes">
|
||||
@foreach (var theme in _livre.Themes)
|
||||
{
|
||||
<span class="etiquette">@theme</span>
|
||||
}
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_livre.UrlNotice))
|
||||
{
|
||||
<div><dt>Source</dt><dd><a href="@_livre.UrlNotice" target="_blank" rel="noopener noreferrer">Voir la notice source</a></dd></div>
|
||||
@@ -332,6 +345,7 @@ else
|
||||
Editeur = livre.Editeur,
|
||||
Format = livre.Format,
|
||||
TypeDocument = livre.TypeDocument,
|
||||
Themes = livre.Themes.ToList(),
|
||||
Statut = livre.Statut,
|
||||
CoverUrl = livre.CoverUrl,
|
||||
UrlNotice = livre.UrlNotice,
|
||||
|
||||
@@ -31,6 +31,9 @@ public record EnregistrementLivre
|
||||
/// </summary>
|
||||
public List<AuteurSaisi> Auteurs { get; set; } = [];
|
||||
|
||||
/// <summary>Thèmes saisis manuellement, séparés par des virgules dans l'interface.</summary>
|
||||
public List<string> Themes { get; set; } = [];
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
|
||||
public Format Format { get; set; }
|
||||
@@ -67,6 +70,13 @@ public record EnregistrementLivre
|
||||
set => Auteurs = Decouper(value, Auteurs);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string? ThemesTexte
|
||||
{
|
||||
get => Themes.Count == 0 ? null : string.Join(", ", Themes);
|
||||
set => Themes = DecouperThemes(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Découpe une saisie texte en auteurs, en écartant les entrées vides.
|
||||
/// </summary>
|
||||
@@ -99,6 +109,20 @@ public record EnregistrementLivre
|
||||
nom, roles.GetValueOrDefault(NormalisationTexte.Normaliser(nom), RoleAuteur.NonPrecise)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<string> DecouperThemes(string? saisie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saisie))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var vus = new HashSet<string>(StringComparer.Ordinal);
|
||||
return saisie
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(theme => vus.Add(NormalisationTexte.Normaliser(theme)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Un auteur tel qu'il est saisi : son nom, et son rôle sur ce livre.</summary>
|
||||
|
||||
@@ -37,6 +37,8 @@ public record LivreDto
|
||||
/// <summary>Auteurs, dans l'ordre de la couverture. Peut être vide.</summary>
|
||||
public IReadOnlyList<AuteurDto> Auteurs { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<string> Themes { get; init; } = [];
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
public required Format Format { get; init; }
|
||||
|
||||
@@ -56,6 +56,12 @@ public class Livre
|
||||
/// <summary>Auteurs du livre, ordonnés par <see cref="LivreAuteur.Position"/>.</summary>
|
||||
public List<LivreAuteur> Auteurs { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Thèmes du livre. Ils décrivent l'œuvre et sont donc communs au foyer, contrairement aux
|
||||
/// statuts de lecture et aux envies.
|
||||
/// </summary>
|
||||
public List<LivreTheme> Themes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Statuts de lecture, <b>un par personne</b> et seulement pour celles qui en ont posé un.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Shared.Entites;
|
||||
|
||||
/// <summary>Étiquette thématique commune au catalogue.</summary>
|
||||
public class Theme
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Nom { get; set; } = string.Empty;
|
||||
|
||||
public string NomNormalise { get; set; } = string.Empty;
|
||||
|
||||
public List<LivreTheme> Livres { get; set; } = [];
|
||||
|
||||
public void RecalculerFormes()
|
||||
{
|
||||
Nom = Nom.Trim();
|
||||
NomNormalise = NormalisationTexte.Normaliser(Nom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lien entre un livre et une étiquette thématique.</summary>
|
||||
public class LivreTheme
|
||||
{
|
||||
public int LivreId { get; set; }
|
||||
|
||||
public Livre? Livre { get; set; }
|
||||
|
||||
public int ThemeId { get; set; }
|
||||
|
||||
public Theme? Theme { get; set; }
|
||||
}
|
||||
@@ -39,15 +39,44 @@ public class ServiceCatalogueTests : IDisposable
|
||||
string? auteur = null,
|
||||
string? isbn = null,
|
||||
Format format = Format.Physique,
|
||||
Statut? statut = null) => new()
|
||||
Statut? statut = null,
|
||||
params string[] themes) => new()
|
||||
{
|
||||
Titre = titre,
|
||||
Auteur = auteur,
|
||||
Isbn = isbn,
|
||||
Format = format,
|
||||
Statut = statut,
|
||||
Themes = themes.ToList(),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Les_themes_sont_communs_et_normalises()
|
||||
{
|
||||
var premier = (await _service.CreerAsync(
|
||||
Saisie("Dune", themes: ["Science-fiction", "Dark Fantasy"]), "mathieu")).Livre!;
|
||||
var second = (await _service.CreerAsync(
|
||||
Saisie("Fondation", themes: ["science-fiction", "dark fantasy"]), "camille")).Livre!;
|
||||
|
||||
Assert.Equal(["Dark Fantasy", "Science-fiction"], premier.Themes);
|
||||
Assert.Equal(["Dark Fantasy", "Science-fiction"], second.Themes);
|
||||
Assert.Equal(2, await _db.Themes.CountAsync());
|
||||
Assert.Equal(4, await _db.LivreThemes.CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Modifier_remplace_les_themes_du_livre()
|
||||
{
|
||||
var livre = (await _service.CreerAsync(
|
||||
Saisie("Dune", themes: ["Science-fiction", "Space opera"]), "mathieu")).Livre!;
|
||||
|
||||
var modifie = await _service.ModifierAsync(
|
||||
livre.Id, Saisie("Dune", themes: ["space OPERA", "Aventure"]), "mathieu");
|
||||
|
||||
Assert.Equal(["Aventure", "Space opera"], modifie.Livre!.Themes);
|
||||
Assert.Equal(2, await _db.LivreThemes.CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Creer_enregistre_le_livre_et_trace_lauteur_de_la_saisie()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Entites;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
public class ThemesTests
|
||||
{
|
||||
[Fact]
|
||||
public void La_saisie_des_themes_separe_par_virgules_et_dedoublonne_sans_accents()
|
||||
{
|
||||
var themes = EnregistrementLivre.DecouperThemes(
|
||||
"Dark Fantasy, science-fiction, dark fantasy, , SCIENCE FICTION");
|
||||
|
||||
Assert.Equal(["Dark Fantasy", "science-fiction"], themes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_theme_recalcule_sa_forme_normalisee()
|
||||
{
|
||||
var theme = new Theme { Nom = " Cœur d'aventure " };
|
||||
|
||||
theme.RecalculerFormes();
|
||||
|
||||
Assert.Equal("Cœur d'aventure", theme.Nom);
|
||||
Assert.Equal("coeur d aventure", theme.NomNormalise);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user