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
|
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
|
volontairement distincte de la renormalisation automatique : c'est un choix explicite de
|
||||||
l'utilisateur.
|
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**
|
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 à
|
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.
|
afficher, mais une colonne de plus.
|
||||||
- **F2. Des thèmes en étiquettes** (dark fantasy, science-fiction, space opera…). Grosse maille :
|
- ~~**F2. Des thèmes en étiquettes** (dark fantasy, science-fiction, space opera…).~~ Traité le
|
||||||
table de tags, saisie, filtre, et la question de la **portée** (commune ? personnelle ?). Une
|
2026-08-20 : thèmes communs au foyer dans `Theme`/`LivreTheme`, saisis manuellement et
|
||||||
source automatique est douteuse — le Dublin Core BnF porte `dc:subject`, mais avec un vocabulaire
|
normalisés pour éviter les doublons. La BnF n'alimente pas ces étiquettes : son `dc:subject`
|
||||||
Rameau qui ne ressemble pas à ces mots-là. À instruire séparément du reste du lot.
|
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
|
- ~~**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
|
2026-08-20 : le renommage recalcule les formes et propose une fusion confirmable en cas de
|
||||||
collision. ⚠️ Le n-n est déjà
|
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<LivreAuteur> LivreAuteurs => Set<LivreAuteur>();
|
||||||
|
|
||||||
|
public DbSet<Theme> Themes => Set<Theme>();
|
||||||
|
|
||||||
|
public DbSet<LivreTheme> LivreThemes => Set<LivreTheme>();
|
||||||
|
|
||||||
public DbSet<StatutLecture> StatutsLecture => Set<StatutLecture>();
|
public DbSet<StatutLecture> StatutsLecture => Set<StatutLecture>();
|
||||||
|
|
||||||
public DbSet<RapprochementRefuse> RapprochementsRefuses => Set<RapprochementRefuse>();
|
public DbSet<RapprochementRefuse> RapprochementsRefuses => Set<RapprochementRefuse>();
|
||||||
@@ -94,6 +98,30 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
|||||||
lien.HasIndex(la => la.AuteurId);
|
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 =>
|
modelBuilder.Entity<StatutLecture>(statut =>
|
||||||
{
|
{
|
||||||
statut.Property(s => s.Utilisateur).IsRequired();
|
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");
|
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 =>
|
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -417,6 +432,28 @@ namespace MaBibli.Api.Data.Migrations
|
|||||||
b.ToTable("StatutsLecture");
|
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 =>
|
modelBuilder.Entity("MaBibli.Shared.Entites.ElementSerie", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
|
||||||
@@ -454,6 +491,25 @@ namespace MaBibli.Api.Data.Migrations
|
|||||||
b.Navigation("Livre");
|
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 =>
|
modelBuilder.Entity("MaBibli.Shared.Entites.NumeroRevue", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("MaBibli.Shared.Entites.Revue", "Revue")
|
b.HasOne("MaBibli.Shared.Entites.Revue", "Revue")
|
||||||
@@ -509,6 +565,8 @@ namespace MaBibli.Api.Data.Migrations
|
|||||||
b.Navigation("Prets");
|
b.Navigation("Prets");
|
||||||
|
|
||||||
b.Navigation("Statuts");
|
b.Navigation("Statuts");
|
||||||
|
|
||||||
|
b.Navigation("Themes");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
||||||
@@ -522,6 +580,11 @@ namespace MaBibli.Api.Data.Migrations
|
|||||||
|
|
||||||
b.Navigation("SousSeries");
|
b.Navigation("SousSeries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MaBibli.Shared.Entites.Theme", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Livres");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ public sealed class ServiceRenormalisation(MaBibliDbContext db, ILogger<ServiceR
|
|||||||
n => n.RecalculerFormes(),
|
n => n.RecalculerFormes(),
|
||||||
n => $"le numéro {n.Numero}");
|
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)
|
if (corriges > 0)
|
||||||
{
|
{
|
||||||
await db.SaveChangesAsync(ct);
|
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,
|
// AsNoTracking : lecture pure. Et surtout : AUCUN filtre sur AjoutePar,
|
||||||
// la bibliothèque est commune à tout le foyer (CLAUDE.md).
|
// la bibliothèque est commune à tout le foyer (CLAUDE.md).
|
||||||
var requete = FiltreLivres.Appliquer(
|
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,
|
criteres,
|
||||||
utilisateur);
|
utilisateur);
|
||||||
|
|
||||||
@@ -92,6 +94,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
|||||||
var livre = await db.Livres
|
var livre = await db.Livres
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(l => l.Auteurs).ThenInclude(la => la.Auteur)
|
.Include(l => l.Auteurs).ThenInclude(la => la.Auteur)
|
||||||
|
.Include(l => l.Themes).ThenInclude(lt => lt.Theme)
|
||||||
.FirstOrDefaultAsync(l => l.Id == id, ct);
|
.FirstOrDefaultAsync(l => l.Id == id, ct);
|
||||||
|
|
||||||
if (livre is null)
|
if (livre is null)
|
||||||
@@ -143,6 +146,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
|||||||
db.Livres.Add(livre);
|
db.Livres.Add(livre);
|
||||||
|
|
||||||
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
||||||
|
await RattacherThemesAsync(livre, saisie.Themes, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
// Le statut n'est écrit que si l'utilisateur en a explicitement posé un : un livre sans
|
// 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(
|
public async Task<ResultatEcriture> ModifierAsync(
|
||||||
int id, EnregistrementLivre saisie, string? utilisateur, CancellationToken ct = default)
|
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)
|
if (livre is null)
|
||||||
{
|
{
|
||||||
return ResultatEcriture.Introuvable;
|
return ResultatEcriture.Introuvable;
|
||||||
@@ -179,6 +186,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
|||||||
// d'origine, pas des champs éditables.
|
// d'origine, pas des champs éditables.
|
||||||
|
|
||||||
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
await RattacherAuteursAsync(livre, saisie.Auteurs, ct);
|
||||||
|
await RattacherThemesAsync(livre, saisie.Themes, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
// Le statut modifié est celui de la personne qui édite, pas de celle qui a saisi le livre.
|
// 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() =>
|
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>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Contrôles minimaux : un titre, et un ISBN qui tienne debout s'il est fourni.
|
/// Contrôles minimaux : un titre, et un ISBN qui tienne debout s'il est fourni.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -491,6 +532,11 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
|||||||
.Where(la => la.Auteur is not null)
|
.Where(la => la.Auteur is not null)
|
||||||
.Select(la => new AuteurDto { Id = la.Auteur!.Id, Nom = la.Auteur.Nom, Role = la.Role })
|
.Select(la => new AuteurDto { Id = la.Auteur!.Id, Nom = la.Auteur.Nom, Role = la.Role })
|
||||||
.ToList(),
|
.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,
|
Editeur = livre.Editeur,
|
||||||
Format = livre.Format,
|
Format = livre.Format,
|
||||||
TypeDocument = livre.TypeDocument,
|
TypeDocument = livre.TypeDocument,
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
@Libelles.TypeDocument(Livre.TypeDocument)
|
@Libelles.TypeDocument(Livre.TypeDocument)
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@foreach (var theme in Livre.Themes)
|
||||||
|
{
|
||||||
|
<span class="etiquette">@theme</span>
|
||||||
|
}
|
||||||
@if (Livre.Statut is { } statut)
|
@if (Livre.Statut is { } statut)
|
||||||
{
|
{
|
||||||
<span class="etiquette etiquette-statut @Libelles.ClasseStatut(statut)">
|
<span class="etiquette etiquette-statut @Libelles.ClasseStatut(statut)">
|
||||||
|
|||||||
@@ -69,6 +69,12 @@
|
|||||||
autocomplete="off" />
|
autocomplete="off" />
|
||||||
</label>
|
</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">
|
<label class="champ">
|
||||||
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
|
<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"
|
<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>
|
<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))
|
@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>
|
<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,
|
Editeur = livre.Editeur,
|
||||||
Format = livre.Format,
|
Format = livre.Format,
|
||||||
TypeDocument = livre.TypeDocument,
|
TypeDocument = livre.TypeDocument,
|
||||||
|
Themes = livre.Themes.ToList(),
|
||||||
Statut = livre.Statut,
|
Statut = livre.Statut,
|
||||||
CoverUrl = livre.CoverUrl,
|
CoverUrl = livre.CoverUrl,
|
||||||
UrlNotice = livre.UrlNotice,
|
UrlNotice = livre.UrlNotice,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public record EnregistrementLivre
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<AuteurSaisi> Auteurs { get; set; } = [];
|
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 string? Editeur { get; set; }
|
||||||
|
|
||||||
public Format Format { get; set; }
|
public Format Format { get; set; }
|
||||||
@@ -67,6 +70,13 @@ public record EnregistrementLivre
|
|||||||
set => Auteurs = Decouper(value, Auteurs);
|
set => Auteurs = Decouper(value, Auteurs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? ThemesTexte
|
||||||
|
{
|
||||||
|
get => Themes.Count == 0 ? null : string.Join(", ", Themes);
|
||||||
|
set => Themes = DecouperThemes(value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Découpe une saisie texte en auteurs, en écartant les entrées vides.
|
/// Découpe une saisie texte en auteurs, en écartant les entrées vides.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -99,6 +109,20 @@ public record EnregistrementLivre
|
|||||||
nom, roles.GetValueOrDefault(NormalisationTexte.Normaliser(nom), RoleAuteur.NonPrecise)))
|
nom, roles.GetValueOrDefault(NormalisationTexte.Normaliser(nom), RoleAuteur.NonPrecise)))
|
||||||
.ToList();
|
.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>
|
/// <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>
|
/// <summary>Auteurs, dans l'ordre de la couverture. Peut être vide.</summary>
|
||||||
public IReadOnlyList<AuteurDto> Auteurs { get; init; } = [];
|
public IReadOnlyList<AuteurDto> Auteurs { get; init; } = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Themes { get; init; } = [];
|
||||||
|
|
||||||
public string? Editeur { get; init; }
|
public string? Editeur { get; init; }
|
||||||
|
|
||||||
public required Format Format { 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>
|
/// <summary>Auteurs du livre, ordonnés par <see cref="LivreAuteur.Position"/>.</summary>
|
||||||
public List<LivreAuteur> Auteurs { get; set; } = [];
|
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>
|
/// <summary>
|
||||||
/// Statuts de lecture, <b>un par personne</b> et seulement pour celles qui en ont posé un.
|
/// Statuts de lecture, <b>un par personne</b> et seulement pour celles qui en ont posé un.
|
||||||
/// </summary>
|
/// </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? auteur = null,
|
||||||
string? isbn = null,
|
string? isbn = null,
|
||||||
Format format = Format.Physique,
|
Format format = Format.Physique,
|
||||||
Statut? statut = null) => new()
|
Statut? statut = null,
|
||||||
|
params string[] themes) => new()
|
||||||
{
|
{
|
||||||
Titre = titre,
|
Titre = titre,
|
||||||
Auteur = auteur,
|
Auteur = auteur,
|
||||||
Isbn = isbn,
|
Isbn = isbn,
|
||||||
Format = format,
|
Format = format,
|
||||||
Statut = statut,
|
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]
|
[Fact]
|
||||||
public async Task Creer_enregistre_le_livre_et_trace_lauteur_de_la_saisie()
|
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