Ajoute les notices sources et le renommage fusionné des auteurs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
6b8f08ef26
commit
e28d5bce32
@@ -1981,3 +1981,30 @@ exception explicite à la règle générale « griser, ne jamais masquer ».
|
||||
Enfin, plusieurs œuvres non possédées peuvent être cochées puis ajoutées aux envies en une seule
|
||||
action d'écran. Chaque ajout reprend le même rapprochement et les mêmes validations que l'ajout
|
||||
unitaire ; les doublons sont rapportés sans effacer la sélection des autres œuvres.
|
||||
|
||||
## Lot F — enrichir la fiche d'un livre (F1 et F3, 2026-08-20)
|
||||
|
||||
### F1 : lien vers la notice source
|
||||
|
||||
`Livre.UrlNotice` conserve l'URL de la notice utilisée pour préremplir une fiche depuis un
|
||||
lookup ISBN. La BnF expose dans `srw:recordIdentifier` un ARK stable
|
||||
(`ark:/12148/cb…`) : il est transformé en `https://catalogue.bnf.fr/{ark}`. OpenLibrary expose
|
||||
la clé d'édition (`/books/OL…M`) : elle devient
|
||||
`https://openlibrary.org/{clé}`. Ce sont des pages de notice directement consultables, et non
|
||||
les endpoints JSON.
|
||||
|
||||
L'URL traverse `CandidatLivre`, `EnregistrementLivre` et le client jusqu'à la création du livre.
|
||||
Une saisie manuelle laisse la colonne nulle. Les livres déjà en base n'ont pas cette information :
|
||||
aucun rétro-remplissage n'est possible puisque la notice choisie n'a pas été conservée à l'époque.
|
||||
La fiche affiche « Voir la notice source » uniquement quand l'URL existe.
|
||||
|
||||
### F3 : renommer et fusionner un auteur
|
||||
|
||||
`PUT /api/auteurs/{id}` recalcule `NomNormalise` et `CleRegroupement`. Si la clé de regroupement
|
||||
est déjà portée par une autre fiche, l'API répond `409` avec la fiche cible ; l'écran explique
|
||||
la fusion et demande confirmation. La confirmation réutilise la fusion existante : tous les liens
|
||||
`LivreAuteur` sont réattribués, les doublons de lien pour un même livre sont réunis en conservant
|
||||
la position la plus précoce et un rôle renseigné plutôt que `NonPrecise`, puis la fiche source et
|
||||
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.
|
||||
|
||||
@@ -233,7 +233,9 @@ L'écran `/souhaits/ajout` rend aujourd'hui une liste plate.
|
||||
|
||||
## Lot F — Enrichir la fiche d'un livre
|
||||
|
||||
- **F1. Un lien vers la notice source** (BnF, OpenLibrary) sur la fiche. Peu coûteux, et c'est le
|
||||
- ~~**F1. Un lien vers la notice source** (BnF, OpenLibrary) sur la fiche.~~ Traité le 2026-08-20 :
|
||||
l'URL est conservée à la création quand le livre vient d'un lookup ISBN et affichée sur la fiche.
|
||||
Peu coûteux, et c'est le
|
||||
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.
|
||||
@@ -241,7 +243,9 @@ L'écran `/souhaits/ajout` rend aujourd'hui une liste plate.
|
||||
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.
|
||||
- **F3. Renommer un auteur**, la correction se répercutant sur tous ses livres. ⚠️ Le n-n est déjà
|
||||
- ~~**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à
|
||||
en place, donc un seul `Auteur.Nom` à changer — mais **`NomNormalise` et `CleRegroupement` se
|
||||
recalculent**, et la clé porte un index **unique**. Renommer « Hamilton » en « Peter F. Hamilton »
|
||||
peut donc **entrer en collision** avec une fiche existante : le geste est alors une **fusion**,
|
||||
|
||||
@@ -50,6 +50,7 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
{
|
||||
livre.Property(l => l.Titre).IsRequired();
|
||||
livre.Property(l => l.TitreNormalise).IsRequired();
|
||||
livre.Property(l => l.UrlNotice);
|
||||
livre.HasIndex(l => l.Isbn);
|
||||
|
||||
// Le tri et la recherche passent par là : sans index, chaque frappe balaie la table.
|
||||
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
// <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("20260820115411_NoticeBibliographique")]
|
||||
partial class NoticeBibliographique
|
||||
{
|
||||
/// <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.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.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.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");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Revue", b =>
|
||||
{
|
||||
b.Navigation("Numeros");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MaBibli.Shared.Entites.Serie", b =>
|
||||
{
|
||||
b.Navigation("Elements");
|
||||
|
||||
b.Navigation("SousSeries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MaBibli.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class NoticeBibliographique : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UrlNotice",
|
||||
table: "Livres",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UrlNotice",
|
||||
table: "Livres");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,9 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.Property<int>("TypeDocument")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UrlNotice")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Isbn");
|
||||
|
||||
@@ -186,6 +186,31 @@ public static class LivresEndpoints
|
||||
.Produces<AuteurDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
groupe.MapPut("/{id:int}", async (
|
||||
int id,
|
||||
RenommerAuteur demande,
|
||||
bool? confirmerFusion,
|
||||
IServiceAuteurs service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var resultat = await service.RenommerAsync(
|
||||
id, demande.Nom, confirmerFusion ?? false, ct);
|
||||
|
||||
if (resultat.Auteur is null && resultat.Collision is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return resultat.Collision is not null
|
||||
? Results.Conflict(resultat)
|
||||
: Results.Ok(resultat);
|
||||
})
|
||||
.WithName("RenommerAuteur")
|
||||
.WithSummary("Renomme un auteur ou fusionne sa fiche avec une fiche existante.")
|
||||
.Produces<ResultatRenommageAuteur>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status409Conflict);
|
||||
|
||||
groupe.MapGet("/rapprochements", async (IServiceAuteurs service, CancellationToken ct) =>
|
||||
Results.Ok(await service.SuggestionsAsync(ct)))
|
||||
.WithName("ListerRapprochements")
|
||||
|
||||
@@ -18,6 +18,10 @@ public interface IServiceAuteurs
|
||||
|
||||
Task<AuteurDto?> ObtenirAsync(int id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Renomme une fiche, ou signale la fiche cible à fusionner.</summary>
|
||||
Task<ResultatRenommageAuteur> RenommerAsync(
|
||||
int id, string nom, bool confirmerFusion = false, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Rapprochements ambigus à soumettre à l'utilisateur, refus déjà écartés.</summary>
|
||||
Task<IReadOnlyList<SuggestionRapprochement>> SuggestionsAsync(CancellationToken ct = default);
|
||||
|
||||
@@ -139,6 +143,62 @@ public sealed class ServiceAuteurs(MaBibliDbContext db) : IServiceAuteurs
|
||||
})
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
public async Task<ResultatRenommageAuteur> RenommerAsync(
|
||||
int id, string nom, bool confirmerFusion = false, CancellationToken ct = default)
|
||||
{
|
||||
var auteur = await db.Auteurs.FirstOrDefaultAsync(a => a.Id == id, ct);
|
||||
if (auteur is null)
|
||||
{
|
||||
return new ResultatRenommageAuteur();
|
||||
}
|
||||
|
||||
nom = nom.Trim();
|
||||
var cle = RapprochementAuteurs.Cle(nom);
|
||||
if (cle.Length == 0)
|
||||
{
|
||||
return new ResultatRenommageAuteur
|
||||
{
|
||||
Auteur = new AuteurDto { Id = id, Nom = auteur.Nom },
|
||||
};
|
||||
}
|
||||
|
||||
var collision = await db.Auteurs
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.Id != id && a.CleRegroupement == cle, ct);
|
||||
|
||||
if (collision is not null)
|
||||
{
|
||||
var dto = new AuteurDto
|
||||
{
|
||||
Id = collision.Id,
|
||||
Nom = collision.Nom,
|
||||
NombreDeLivres = await db.LivreAuteurs.CountAsync(l => l.AuteurId == collision.Id, ct),
|
||||
};
|
||||
|
||||
if (!confirmerFusion)
|
||||
{
|
||||
return new ResultatRenommageAuteur { Collision = dto };
|
||||
}
|
||||
|
||||
await FusionnerAsync(id, collision.Id, ct);
|
||||
return new ResultatRenommageAuteur { Auteur = dto };
|
||||
}
|
||||
|
||||
auteur.Nom = nom;
|
||||
auteur.RecalculerFormes();
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return new ResultatRenommageAuteur
|
||||
{
|
||||
Auteur = new AuteurDto
|
||||
{
|
||||
Id = auteur.Id,
|
||||
Nom = auteur.Nom,
|
||||
NombreDeLivres = await db.LivreAuteurs.CountAsync(l => l.AuteurId == auteur.Id, ct),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SuggestionRapprochement>> SuggestionsAsync(
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -217,6 +277,15 @@ public sealed class ServiceAuteurs(MaBibliDbContext db) : IServiceAuteurs
|
||||
// la clé primaire du lien est (LivreId, AuteurId).
|
||||
if (!dejaLies.Add(lien.LivreId))
|
||||
{
|
||||
var existant = cible.Livres.FirstOrDefault(l => l.LivreId == lien.LivreId);
|
||||
if (existant is not null)
|
||||
{
|
||||
existant.Position = Math.Min(existant.Position, lien.Position);
|
||||
if (existant.Role == RoleAuteur.NonPrecise)
|
||||
{
|
||||
existant.Role = lien.Role;
|
||||
}
|
||||
}
|
||||
db.LivreAuteurs.Remove(lien);
|
||||
continue;
|
||||
}
|
||||
@@ -227,6 +296,7 @@ public sealed class ServiceAuteurs(MaBibliDbContext db) : IServiceAuteurs
|
||||
LivreId = lien.LivreId,
|
||||
AuteurId = cible.Id,
|
||||
Position = lien.Position,
|
||||
Role = lien.Role,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
Format = saisie.Format,
|
||||
TypeDocument = saisie.TypeDocument,
|
||||
CoverUrl = Vide(saisie.CoverUrl),
|
||||
UrlNotice = Vide(saisie.UrlNotice),
|
||||
DateAjout = DateTime.UtcNow,
|
||||
|
||||
// Renseigné par le serveur à partir de l'utilisateur authentifié, jamais par le client.
|
||||
@@ -171,6 +172,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
livre.Format = saisie.Format;
|
||||
livre.TypeDocument = saisie.TypeDocument;
|
||||
livre.CoverUrl = Vide(saisie.CoverUrl);
|
||||
livre.UrlNotice = Vide(saisie.UrlNotice);
|
||||
livre.RecalculerFormes();
|
||||
|
||||
// DateAjout et AjoutePar ne sont jamais réécrits : ce sont des traces de la saisie
|
||||
@@ -494,6 +496,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
TypeDocument = livre.TypeDocument,
|
||||
Statut = statut,
|
||||
CoverUrl = livre.CoverUrl,
|
||||
UrlNotice = livre.UrlNotice,
|
||||
DateAjout = livre.DateAjout,
|
||||
AjoutePar = livre.AjoutePar,
|
||||
PreteA = pretOuvert?.Emprunteur,
|
||||
|
||||
@@ -62,6 +62,8 @@ public static class BnfSruParser
|
||||
continue;
|
||||
}
|
||||
|
||||
var identifiant = record.Element(Srw + "recordIdentifier")?.Value?.Trim();
|
||||
|
||||
candidats.Add(new CandidatLivre
|
||||
{
|
||||
Titre = titre,
|
||||
@@ -72,13 +74,19 @@ public static class BnfSruParser
|
||||
CoverUrl = urlCouverture,
|
||||
Source = SourceMetadonnees.Bnf,
|
||||
IsbnInterroge = isbnInterroge,
|
||||
IdentifiantSource = record.Element(Srw + "recordIdentifier")?.Value?.Trim(),
|
||||
IdentifiantSource = identifiant,
|
||||
UrlNotice = UrlNotice(identifiant),
|
||||
});
|
||||
}
|
||||
|
||||
return candidats;
|
||||
}
|
||||
|
||||
private static string? UrlNotice(string? identifiant) =>
|
||||
string.IsNullOrWhiteSpace(identifiant)
|
||||
? null
|
||||
: $"https://catalogue.bnf.fr/{identifiant}";
|
||||
|
||||
/// <summary>
|
||||
/// Lit la <b>première</b> notice d'une réponse SRU interrogée par <c>bib.issn</c>.
|
||||
/// </summary>
|
||||
|
||||
@@ -83,9 +83,15 @@ public static class OpenLibraryMapper
|
||||
Source = SourceMetadonnees.OpenLibrary,
|
||||
IsbnInterroge = isbnInterroge,
|
||||
IdentifiantSource = edition.Key,
|
||||
UrlNotice = UrlNotice(edition.Key),
|
||||
};
|
||||
}
|
||||
|
||||
private static string? UrlNotice(string? cle) =>
|
||||
string.IsNullOrWhiteSpace(cle)
|
||||
? null
|
||||
: $"https://openlibrary.org/{cle.TrimStart('/')}";
|
||||
|
||||
private static IReadOnlyList<string> Liste(string? valeur) =>
|
||||
string.IsNullOrWhiteSpace(valeur) ? [] : [valeur];
|
||||
|
||||
|
||||
@@ -431,6 +431,7 @@
|
||||
Auteur = candidat.Auteur,
|
||||
Editeur = candidat.Editeur,
|
||||
CoverUrl = candidat.CoverUrl,
|
||||
UrlNotice = candidat.UrlNotice,
|
||||
Format = Format.Physique,
|
||||
Statut = Statut.ALire,
|
||||
};
|
||||
|
||||
@@ -79,15 +79,61 @@ else
|
||||
tout ce qu'il a ecrit et marquer ce qui manque. *@
|
||||
<a class="bouton bouton-compact bouton-discret"
|
||||
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
|
||||
@if (_auteurRenomme == auteur.Id)
|
||||
{
|
||||
<input class="champ-texte champ-auteur-renomme" @bind="_nomRenomme" />
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_renommageEnCours" @onclick="RenommerAsync">
|
||||
Enregistrer
|
||||
</button>
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@_renommageEnCours" @onclick="AnnulerRenommage">
|
||||
Annuler
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@(_enCours || !Reseau.EnLigne)"
|
||||
title="@MotifBlocage"
|
||||
@onclick="() => CommencerRenommage(auteur)">
|
||||
Renommer
|
||||
</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (_collisionRenommage is not null)
|
||||
{
|
||||
<section class="message-avertissement" role="alert">
|
||||
<p>
|
||||
Un auteur existe déjà sous le nom « @_collisionRenommage.Nom ».
|
||||
Les livres seront regroupés sous cette fiche.
|
||||
Confirmer la fusion ?
|
||||
</p>
|
||||
<div class="actions-formulaire">
|
||||
<button type="button" class="bouton bouton-principal"
|
||||
disabled="@_renommageEnCours" @onclick="ConfirmerFusionAsync">
|
||||
Oui, fusionner
|
||||
</button>
|
||||
<button type="button" class="bouton bouton-discret"
|
||||
disabled="@_renommageEnCours" @onclick="AnnulerRenommage">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<AuteurDto>? _auteurs;
|
||||
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
|
||||
private bool _enCours;
|
||||
private bool _renommageEnCours;
|
||||
private int? _auteurRenomme;
|
||||
private string _nomRenomme = string.Empty;
|
||||
private AuteurDto? _collisionRenommage;
|
||||
private string? _erreur;
|
||||
|
||||
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
|
||||
@@ -128,6 +174,55 @@ else
|
||||
private Task RefuserAsync(SuggestionRapprochement suggestion) =>
|
||||
DeciderAsync(() => Api.RefuserRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));
|
||||
|
||||
private void CommencerRenommage(AuteurDto auteur)
|
||||
{
|
||||
_auteurRenomme = auteur.Id;
|
||||
_nomRenomme = auteur.Nom;
|
||||
_collisionRenommage = null;
|
||||
_erreur = null;
|
||||
}
|
||||
|
||||
private void AnnulerRenommage()
|
||||
{
|
||||
_auteurRenomme = null;
|
||||
_collisionRenommage = null;
|
||||
_nomRenomme = string.Empty;
|
||||
}
|
||||
|
||||
private Task RenommerAsync() => EnvoyerRenommageAsync(false);
|
||||
|
||||
private Task ConfirmerFusionAsync() => EnvoyerRenommageAsync(true);
|
||||
|
||||
private async Task EnvoyerRenommageAsync(bool confirmerFusion)
|
||||
{
|
||||
if (_auteurRenomme is null || string.IsNullOrWhiteSpace(_nomRenomme))
|
||||
{
|
||||
_erreur = "Le nom de l'auteur est obligatoire.";
|
||||
return;
|
||||
}
|
||||
|
||||
_renommageEnCours = true;
|
||||
_erreur = null;
|
||||
var resultat = await Api.RenommerAuteurAsync(
|
||||
_auteurRenomme.Value, _nomRenomme, confirmerFusion);
|
||||
_renommageEnCours = false;
|
||||
|
||||
if (resultat is null)
|
||||
{
|
||||
_erreur = "Le renommage n'a pas pu être enregistré.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultat.Collision is not null)
|
||||
{
|
||||
_collisionRenommage = resultat.Collision;
|
||||
return;
|
||||
}
|
||||
|
||||
AnnulerRenommage();
|
||||
await ChargerAsync();
|
||||
}
|
||||
|
||||
private async Task DeciderAsync(Func<Task<string?>> action)
|
||||
{
|
||||
_enCours = true;
|
||||
|
||||
@@ -149,6 +149,11 @@ else
|
||||
{
|
||||
<div><dt>Type</dt><dd>@Libelles.TypeDocument(_livre.TypeDocument)</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>
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,6 +334,7 @@ else
|
||||
TypeDocument = livre.TypeDocument,
|
||||
Statut = livre.Statut,
|
||||
CoverUrl = livre.CoverUrl,
|
||||
UrlNotice = livre.UrlNotice,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -646,6 +646,7 @@ else
|
||||
Auteurs = candidat.Auteurs.Select(a => new AuteurSaisi(a)).ToList(),
|
||||
Editeur = candidat.Editeur,
|
||||
CoverUrl = candidat.CoverUrl,
|
||||
UrlNotice = candidat.UrlNotice,
|
||||
};
|
||||
var resultat = await Api.CreerAsync(saisie);
|
||||
_enCours = false;
|
||||
|
||||
@@ -240,6 +240,41 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
return instantane?.Donnees.FirstOrDefault(a => a.Id == id);
|
||||
}
|
||||
|
||||
public async Task<ResultatRenommageAuteur?> RenommerAuteurAsync(
|
||||
int id, string nom, bool confirmerFusion = false, CancellationToken ct = default)
|
||||
{
|
||||
if (!reseau.EnLigne)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var suffixe = confirmerFusion ? "?confirmerFusion=true" : string.Empty;
|
||||
try
|
||||
{
|
||||
var reponse = await http.PutAsJsonAsync(
|
||||
$"api/auteurs/{id}{suffixe}", new RenommerAuteur(nom), Json, ct);
|
||||
reseau.SignalerSuccesReseau();
|
||||
|
||||
if (reponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reponse.StatusCode == HttpStatusCode.Conflict
|
||||
|| reponse.IsSuccessStatusCode)
|
||||
{
|
||||
return await LireAsync<ResultatRenommageAuteur>(reponse, ct);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
reseau.SignalerEchecReseau();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rapprochements d'auteurs ambigus, que le serveur refuse d'appliquer seul.
|
||||
/// </summary>
|
||||
|
||||
@@ -60,4 +60,7 @@ public record CandidatLivre
|
||||
|
||||
/// <summary>Identifiant chez la source : ark BnF (<c>ark:/12148/cb…</c>) ou clé OpenLibrary (<c>/books/OL…M</c>).</summary>
|
||||
public string? IdentifiantSource { get; init; }
|
||||
|
||||
/// <summary>URL de consultation de la notice chez la source.</summary>
|
||||
public string? UrlNotice { get; init; }
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ public record EnregistrementLivre
|
||||
|
||||
public string? CoverUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Notice ayant servi au préremplissage, ou <c>null</c> pour une saisie manuelle.
|
||||
/// </summary>
|
||||
public string? UrlNotice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Vue « une seule ligne » de <see cref="Auteurs"/>, pour les champs de saisie simples.
|
||||
/// </summary>
|
||||
@@ -103,6 +108,19 @@ public record EnregistrementLivre
|
||||
/// </remarks>
|
||||
public record AuteurSaisi(string Nom, RoleAuteur Role = RoleAuteur.NonPrecise);
|
||||
|
||||
/// <summary>Demande de renommage d'une fiche auteur.</summary>
|
||||
public record RenommerAuteur(string Nom);
|
||||
|
||||
/// <summary>Résultat d'un renommage, avec collision éventuelle à confirmer.</summary>
|
||||
public record ResultatRenommageAuteur
|
||||
{
|
||||
public AuteurDto? Auteur { get; init; }
|
||||
|
||||
public AuteurDto? Collision { get; init; }
|
||||
|
||||
public bool EstCollision => Collision is not null;
|
||||
}
|
||||
|
||||
/// <summary>Charge utile du changement de statut de lecture — l'action la plus fréquente.</summary>
|
||||
/// <remarks>
|
||||
/// Le statut posé est toujours celui de l'appelant : il n'y a pas de champ « utilisateur »,
|
||||
|
||||
@@ -58,6 +58,8 @@ public record LivreDto
|
||||
|
||||
public string? CoverUrl { get; init; }
|
||||
|
||||
public string? UrlNotice { get; init; }
|
||||
|
||||
public required DateTime DateAjout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -40,6 +40,9 @@ public class Livre
|
||||
/// <summary>URL de la couverture (OpenLibrary), pas de fichier stocké localement.</summary>
|
||||
public string? CoverUrl { get; set; }
|
||||
|
||||
/// <summary>URL de la notice bibliographique qui a servi à préremplir la fiche.</summary>
|
||||
public string? UrlNotice { get; set; }
|
||||
|
||||
public DateTime DateAjout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,6 +36,7 @@ public class BnfSruParserTests
|
||||
Assert.Equal("9782070612758", c.IsbnInterroge);
|
||||
Assert.Equal("https://couverture", c.CoverUrl);
|
||||
Assert.StartsWith("ark:/12148/", c.IdentifiantSource);
|
||||
Assert.Equal($"https://catalogue.bnf.fr/{c.IdentifiantSource}", c.UrlNotice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -43,6 +43,7 @@ public class OpenLibraryMapperTests
|
||||
Assert.Equal("March 2007", c.Annee);
|
||||
Assert.Equal("fre", c.Langue);
|
||||
Assert.Equal(SourceMetadonnees.OpenLibrary, c.Source);
|
||||
Assert.Equal("https://openlibrary.org/books/OL9567312M", c.UrlNotice);
|
||||
Assert.Equal("/books/OL9567312M", c.IdentifiantSource);
|
||||
Assert.Equal("https://couverture", c.CoverUrl);
|
||||
}
|
||||
|
||||
@@ -189,6 +189,45 @@ public class ServiceAuteursTests : IDisposable
|
||||
Assert.Single(livre.Auteurs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renommer_un_auteur_recalcule_ses_formes()
|
||||
{
|
||||
var livre = await AjouterAsync("Pandora's Star", "Hamilton");
|
||||
|
||||
var resultat = await _auteurs.RenommerAsync(
|
||||
livre.Auteurs.Single().Id, "Peter F. Hamilton");
|
||||
|
||||
Assert.False(resultat.EstCollision);
|
||||
Assert.Equal("Peter F. Hamilton", resultat.Auteur!.Nom);
|
||||
|
||||
var auteur = await _db.Auteurs.SingleAsync();
|
||||
Assert.Equal("peter f hamilton", auteur.NomNormalise);
|
||||
Assert.Equal("f hamilton peter", auteur.CleRegroupement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renommer_avec_collision_demande_puis_fusionne()
|
||||
{
|
||||
var court = await AjouterAsync("Judas Unchained", "Hamilton");
|
||||
var complet = await AjouterAsync("Pandora's Star", "Peter F. Hamilton");
|
||||
var idCourt = court.Auteurs.Single().Id;
|
||||
var idComplet = complet.Auteurs.Single().Id;
|
||||
|
||||
var avertissement = await _auteurs.RenommerAsync(idCourt, "Peter F. Hamilton");
|
||||
|
||||
Assert.True(avertissement.EstCollision);
|
||||
Assert.Equal(idComplet, avertissement.Collision!.Id);
|
||||
Assert.Equal(2, await _db.Auteurs.CountAsync());
|
||||
|
||||
var fusion = await _auteurs.RenommerAsync(
|
||||
idCourt, "Peter F. Hamilton", confirmerFusion: true);
|
||||
|
||||
Assert.False(fusion.EstCollision);
|
||||
Assert.Single(await _db.Auteurs.ToListAsync());
|
||||
Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
|
||||
Assert.Equal(2, (await _auteurs.ListerAsync()).Single().NombreDeLivres);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -70,6 +70,25 @@ public class ServiceCatalogueTests : IDisposable
|
||||
Assert.Equal("9782253004226", resultat.Livre!.Isbn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Creer_conserve_lurl_de_la_notice_source()
|
||||
{
|
||||
var resultat = await _service.CreerAsync(
|
||||
new EnregistrementLivre
|
||||
{
|
||||
Titre = "Germinal",
|
||||
UrlNotice = "https://catalogue.bnf.fr/ark:/12148/cb34676750b",
|
||||
},
|
||||
"mathieu");
|
||||
|
||||
Assert.Equal(
|
||||
"https://catalogue.bnf.fr/ark:/12148/cb34676750b",
|
||||
resultat.Livre!.UrlNotice);
|
||||
Assert.Equal(
|
||||
resultat.Livre.UrlNotice,
|
||||
(await _db.Livres.SingleAsync()).UrlNotice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Creer_refuse_un_titre_vide()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user