Distinguer scénario, dessin et couleurs sur chaque livre
Le rôle vit sur le LIEN livre-auteur, jamais sur l'auteur : le même auteur scénarise un album et dessine le suivant, et il ne doit pas exister en deux fiches pour autant — ce que l'index unique sur la clé de regroupement interdit de toute façon. Les rôles se retrouvent par clé d'auteur résolue et non par position : ResoudreAsync écarte les noms vides et fusionne les variantes, donc sa liste peut être plus courte que la saisie. Piège traité : le champ des noms reconstruit la liste à chaque frappe. Decouper reprend les rôles connus en rapprochant sur la forme normalisée, sans quoi corriger une faute de frappe les effacerait tous. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -731,7 +731,9 @@ Auteur
|
||||
LivreAuteur (n-n : un livre peut avoir plusieurs auteurs)
|
||||
├── LivreId ─┐ clé primaire composite
|
||||
├── AuteurId ─┘ FK Livre = cascade, FK Auteur = restrict
|
||||
└── Position (rang sur la couverture, à partir de 0)
|
||||
├── Position (rang sur la couverture, à partir de 0)
|
||||
└── Role : NonPrecise | Scenario | Dessin | Couleurs
|
||||
⚠ sur le LIEN, pas sur l'auteur : on scénarise un album et on dessine le suivant
|
||||
|
||||
StatutLecture (le statut est PERSONNEL)
|
||||
├── Id
|
||||
@@ -941,6 +943,47 @@ livre sorti depuis six mois qu'on avait oublié, pas celui prêté hier.
|
||||
(`ServiceLivresApi.EnUtc`). Sans cela le prêt se décalerait d'un jour pour la moitié du globe.
|
||||
Vérifié en exécution : saisie du 12/08 → `2026-08-11 22:00` en base (CEST).
|
||||
|
||||
## Rôles des auteurs — migration `RolesDesAuteurs` (2026-08-19)
|
||||
|
||||
Besoin venu de la bande dessinée : savoir **qui écrit et qui dessine**. `LivreAuteur.Role` porte
|
||||
`NonPrecise` (0), `Scenario`, `Dessin`, `Couleurs` — un troisième nom figurant souvent sur un
|
||||
album.
|
||||
|
||||
### ⚠️ Le rôle vit sur le LIEN, pas sur l'auteur
|
||||
|
||||
C'est la décision structurante, et elle se justifie par un cas banal : le même auteur scénarise
|
||||
un album et **dessine** le suivant. Un rôle posé sur `Auteur` obligerait à créer deux fiches pour
|
||||
une seule personne — exactement ce que l'index unique `CleRegroupement` interdit, et ce que tout
|
||||
le travail de rapprochement cherche à éviter.
|
||||
|
||||
Conséquence dans `ServiceCatalogue` : `ResoudreAsync` ne reçoit **que des noms**, et les rôles
|
||||
sont réappliqués ensuite. ⚠️ Ils se retrouvent **par clé d'auteur résolue**, jamais par position :
|
||||
`ResoudreAsync` écarte les noms vides et fusionne les variantes, donc sa liste peut être plus
|
||||
courte que la saisie — un rapprochement par index décalerait tous les rôles suivants. Un test
|
||||
couvre le cas « Maupassant, Guy de » rapproché de « Guy de Maupassant ».
|
||||
|
||||
### Deux règles d'interface, héritées telles quelles
|
||||
|
||||
- **Les rôles n'apparaissent qu'à partir de deux auteurs.** Un roman n'a qu'un signataire dont le
|
||||
rôle ne se pose pas ; la question « qui écrit, qui dessine ? » ne se pose qu'à plusieurs.
|
||||
- **Seul ce qui a été choisi s'affiche** : « Dobbs (scénario), Christophe Regnault (dessin) »,
|
||||
et rien du tout quand le rôle est `NonPrecise`. Même règle que le format et le type de document.
|
||||
|
||||
### ⚠️ Le piège : le champ des noms se relie à chaque frappe
|
||||
|
||||
`EnregistrementLivre.Auteur` (la vue « une seule ligne ») **reconstruit la liste entière** à
|
||||
chaque caractère tapé. Sans précaution, corriger une faute de frappe remettrait tous les rôles à
|
||||
« non précisé » sous les yeux de l'utilisateur.
|
||||
|
||||
`Decouper` reprend donc les rôles des auteurs déjà saisis, en rapprochant sur la forme
|
||||
**normalisée** du nom — ce qui les fait survivre à un accent ajouté après coup (vérifié en
|
||||
exécution : « Christophe » → « Christophé » garde son rôle). Un nom réellement différent, lui,
|
||||
repart à zéro : on ne devine pas qu'il s'agit de la même personne.
|
||||
|
||||
**Ce qui n'est délibérément pas fait** : ni traducteur, ni préfacier, ni photographe. La liste
|
||||
reste courte tant que le besoin n'est pas constaté — une valeur s'ajoute sans migration, la
|
||||
renommer une fois posée sur des centaines de liens, non.
|
||||
|
||||
## Type de document — migration `TypeDeDocument` (2026-08-19)
|
||||
|
||||
Besoin remonté : distinguer une BD d'un roman. Une BD était déjà catalogable telle quelle (elle
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
// <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("20260819203118_RolesDesAuteurs")]
|
||||
partial class RolesDesAuteurs
|
||||
{
|
||||
/// <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.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.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 RolesDesAuteurs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Role",
|
||||
table: "LivreAuteurs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Role",
|
||||
table: "LivreAuteurs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,9 @@ namespace MaBibli.Api.Data.Migrations
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LivreId", "AuteurId");
|
||||
|
||||
b.HasIndex("AuteurId");
|
||||
|
||||
@@ -288,8 +288,10 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
db.Livres.AsNoTracking().Include(l => l.Auteurs).ThenInclude(la => la.Auteur);
|
||||
|
||||
/// <summary>Un des auteurs du livre catalogué est-il l'un de ceux qu'on est en train de saisir ?</summary>
|
||||
private static bool MemeAuteur(Livre livre, IReadOnlyList<string> saisis) =>
|
||||
RapprochementAuteurs.PartagentUnAuteur(livre.Auteurs.Select(la => la.Auteur?.Nom), saisis);
|
||||
/// <remarks>Le rôle n'entre pas dans la comparaison : c'est la personne qui compte ici.</remarks>
|
||||
private static bool MemeAuteur(Livre livre, IReadOnlyList<AuteurSaisi> saisis) =>
|
||||
RapprochementAuteurs.PartagentUnAuteur(
|
||||
livre.Auteurs.Select(la => la.Auteur?.Nom), saisis.Select(a => a.Nom));
|
||||
|
||||
/// <summary>Phrase d'avertissement, écrite pour être lue telle quelle par l'utilisateur.</summary>
|
||||
/// <remarks>
|
||||
@@ -399,11 +401,18 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>Remplace les auteurs du livre par ceux de la saisie, en conservant leur ordre.</summary>
|
||||
/// <summary>
|
||||
/// Remplace les auteurs du livre par ceux de la saisie, en conservant leur ordre et leur rôle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Le <b>rôle vit sur le lien</b>, pas sur la fiche de l'auteur : le même dessinateur
|
||||
/// peut avoir scénarisé un autre album, et il ne doit pas exister en deux fiches pour
|
||||
/// autant. C'est pour cela que <c>ResoudreAsync</c> ne reçoit que des noms.
|
||||
/// </remarks>
|
||||
private async Task RattacherAuteursAsync(
|
||||
Livre livre, IEnumerable<string> noms, CancellationToken ct)
|
||||
Livre livre, IReadOnlyList<AuteurSaisi> saisis, CancellationToken ct)
|
||||
{
|
||||
var resolus = await auteurs.ResoudreAsync(noms, ct);
|
||||
var resolus = await auteurs.ResoudreAsync(saisis.Select(a => a.Nom), ct);
|
||||
|
||||
// Les liens existants sont retirés puis reconstruits : c'est la façon la plus simple de
|
||||
// gérer à la fois l'ajout, le retrait et le réordonnancement des auteurs.
|
||||
@@ -413,9 +422,23 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
livre.Auteurs.Clear();
|
||||
}
|
||||
|
||||
// ResoudreAsync écarte les noms vides et fusionne les variantes : sa liste peut être
|
||||
// plus courte que la saisie. Les rôles se retrouvent donc par NOM RÉSOLU, jamais par
|
||||
// position — sans quoi un nom vide au milieu décalerait tous les rôles suivants.
|
||||
var roles = new Dictionary<string, RoleAuteur>(StringComparer.Ordinal);
|
||||
foreach (var saisi in saisis)
|
||||
{
|
||||
roles.TryAdd(RapprochementAuteurs.Cle(saisi.Nom), saisi.Role);
|
||||
}
|
||||
|
||||
for (var i = 0; i < resolus.Count; i++)
|
||||
{
|
||||
livre.Auteurs.Add(new LivreAuteur { Auteur = resolus[i], Position = i });
|
||||
livre.Auteurs.Add(new LivreAuteur
|
||||
{
|
||||
Auteur = resolus[i],
|
||||
Position = i,
|
||||
Role = roles.GetValueOrDefault(resolus[i].CleRegroupement, RoleAuteur.NonPrecise),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +487,7 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
Auteurs = livre.Auteurs
|
||||
.OrderBy(la => la.Position)
|
||||
.Where(la => la.Auteur is not null)
|
||||
.Select(la => new AuteurDto { Id = la.Auteur!.Id, Nom = la.Auteur.Nom })
|
||||
.Select(la => new AuteurDto { Id = la.Auteur!.Id, Nom = la.Auteur.Nom, Role = la.Role })
|
||||
.ToList(),
|
||||
Editeur = livre.Editeur,
|
||||
Format = livre.Format,
|
||||
|
||||
@@ -31,6 +31,38 @@
|
||||
placeholder="Prénom Nom ; Prénom Nom" autocomplete="off" />
|
||||
</label>
|
||||
|
||||
@*
|
||||
Les rôles n'apparaissent qu'à partir de DEUX auteurs, et c'est délibéré : un roman n'a
|
||||
qu'un signataire dont le rôle ne se pose pas, et une ligne de boutons sous chaque saisie
|
||||
n'apprendrait rien. La question « qui écrit, qui dessine ? » ne se pose qu'à plusieurs.
|
||||
Le défaut reste « non précisé » : on ne remplit pas une case à la place de l'utilisateur.
|
||||
*@
|
||||
@if (Saisie.Auteurs.Count > 1)
|
||||
{
|
||||
<fieldset class="champ">
|
||||
<legend class="champ-libelle">
|
||||
Rôles<span class="champ-aide"> (facultatif — utile pour une BD)</span>
|
||||
</legend>
|
||||
|
||||
@foreach (var (auteur, index) in Saisie.Auteurs.Select((a, i) => (a, i)))
|
||||
{
|
||||
<div class="ligne-role" @key="index">
|
||||
<span class="nom-role">@auteur.Nom</span>
|
||||
<div class="segments">
|
||||
@foreach (var role in Roles)
|
||||
{
|
||||
<button type="button"
|
||||
class="segment @(auteur.Role == role ? "segment-actif" : null)"
|
||||
@onclick="() => PoserRole(index, role)">
|
||||
@Libelles.RoleAuteur(role)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
<label class="champ">
|
||||
<span class="champ-libelle">Éditeur</span>
|
||||
<input class="champ-saisie" type="text" @bind="Saisie.Editeur" @bind:event="oninput"
|
||||
@@ -135,6 +167,19 @@
|
||||
|
||||
private static readonly TypeDocument[] Types = Enum.GetValues<TypeDocument>();
|
||||
|
||||
private static readonly RoleAuteur[] Roles = Enum.GetValues<RoleAuteur>();
|
||||
|
||||
/// <summary>
|
||||
/// Pose le rôle d'un auteur.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AuteurSaisi"/> est un record : on remplace l'entrée plutôt que de la muter,
|
||||
/// et la liste reste celle que la propriété <c>Auteur</c> saura relire pour préserver les
|
||||
/// rôles à la frappe suivante.
|
||||
/// </remarks>
|
||||
private void PoserRole(int index, RoleAuteur role) =>
|
||||
Saisie.Auteurs[index] = Saisie.Auteurs[index] with { Role = role };
|
||||
|
||||
[Parameter, EditorRequired] public EnregistrementLivre Saisie { get; set; } = new();
|
||||
|
||||
[Parameter] public string LibelleValidation { get; set; } = "Enregistrer";
|
||||
|
||||
@@ -25,6 +25,19 @@ public static class Libelles
|
||||
_ => type.ToString(),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Libellé d'un rôle d'auteur. « Non précisé » ne s'affiche jamais sur une fiche : c'est
|
||||
/// l'absence de choix, pas une information.
|
||||
/// </summary>
|
||||
public static string RoleAuteur(RoleAuteur role) => role switch
|
||||
{
|
||||
MaBibli.Shared.Entites.RoleAuteur.NonPrecise => "Non précisé",
|
||||
MaBibli.Shared.Entites.RoleAuteur.Scenario => "Scénario",
|
||||
MaBibli.Shared.Entites.RoleAuteur.Dessin => "Dessin",
|
||||
MaBibli.Shared.Entites.RoleAuteur.Couleurs => "Couleurs",
|
||||
_ => role.ToString(),
|
||||
};
|
||||
|
||||
public static string Statut(Statut statut) => statut switch
|
||||
{
|
||||
MaBibli.Shared.Entites.Statut.ALire => "À lire",
|
||||
|
||||
@@ -127,7 +127,11 @@ else if (_livres is not null)
|
||||
@foreach (var (auteur, index) in livre.Auteurs.Select((a, i) => (a, i)))
|
||||
{
|
||||
@(index > 0 ? ", " : "")
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>@if (auteur.Role != RoleAuteur.NonPrecise)
|
||||
{
|
||||
@* Comme le format et le type : seul ce qui a été choisi s'affiche. *@
|
||||
<span class="role-auteur"> (@Libelles.RoleAuteur(auteur.Role).ToLowerInvariant())</span>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
@@ -104,7 +104,10 @@ else
|
||||
@foreach (var (auteur, index) in _livre.Auteurs.Select((a, i) => (a, i)))
|
||||
{
|
||||
@(index > 0 ? ", " : "")
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>@if (auteur.Role != RoleAuteur.NonPrecise)
|
||||
{
|
||||
<span class="role-auteur"> (@Libelles.RoleAuteur(auteur.Role).ToLowerInvariant())</span>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@@ -320,7 +323,7 @@ else
|
||||
{
|
||||
Isbn = livre.Isbn,
|
||||
Titre = livre.Titre,
|
||||
Auteurs = livre.Auteurs.Select(a => a.Nom).ToList(),
|
||||
Auteurs = livre.Auteurs.Select(a => new AuteurSaisi(a.Nom, a.Role)).ToList(),
|
||||
Editeur = livre.Editeur,
|
||||
Format = livre.Format,
|
||||
TypeDocument = livre.TypeDocument,
|
||||
|
||||
@@ -1044,3 +1044,25 @@ body {
|
||||
.champs-numero .champ {
|
||||
flex: 1 1 8rem;
|
||||
}
|
||||
|
||||
/* --- Rôles des auteurs (scénario, dessin, couleurs) --- */
|
||||
|
||||
.role-auteur {
|
||||
color: var(--mb-texte-doux);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Un nom, sa rangée de rôles : la ligne se replie sur les écrans étroits plutôt que de
|
||||
comprimer les boutons, qui doivent rester atteignables au pouce. */
|
||||
.ligne-role {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.75rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.nom-role {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MaBibli.Shared.Entites;
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Shared.Dtos;
|
||||
|
||||
@@ -25,10 +26,10 @@ public record EnregistrementLivre
|
||||
public string Titre { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Noms des auteurs, dans l'ordre de la couverture. Le serveur les rattache aux fiches
|
||||
/// existantes quand c'est sûr, et en crée sinon.
|
||||
/// Auteurs, dans l'ordre de la couverture, chacun avec son rôle sur <b>ce</b> livre. Le
|
||||
/// serveur les rattache aux fiches existantes quand c'est sûr, et en crée sinon.
|
||||
/// </summary>
|
||||
public List<string> Auteurs { get; set; } = [];
|
||||
public List<AuteurSaisi> Auteurs { get; set; } = [];
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
|
||||
@@ -57,18 +58,51 @@ public record EnregistrementLivre
|
||||
[JsonIgnore]
|
||||
public string? Auteur
|
||||
{
|
||||
get => Auteurs.Count == 0 ? null : string.Join($"{SeparateurAuteurs} ", Auteurs);
|
||||
set => Auteurs = Decouper(value);
|
||||
get => Auteurs.Count == 0 ? null : string.Join($"{SeparateurAuteurs} ", Auteurs.Select(a => a.Nom));
|
||||
set => Auteurs = Decouper(value, Auteurs);
|
||||
}
|
||||
|
||||
/// <summary>Découpe une saisie texte en noms d'auteurs, en écartant les entrées vides.</summary>
|
||||
public static List<string> Decouper(string? saisie) =>
|
||||
string.IsNullOrWhiteSpace(saisie)
|
||||
? []
|
||||
: saisie.Split(SeparateurAuteurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
/// <summary>
|
||||
/// Découpe une saisie texte en auteurs, en écartant les entrées vides.
|
||||
/// </summary>
|
||||
/// <param name="connus">
|
||||
/// Auteurs déjà saisis, dont les <b>rôles</b> sont repris quand le nom réapparaît.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// ⚠️ Sans cette reprise, corriger une faute de frappe dans le champ des noms effacerait le
|
||||
/// rôle des auteurs voisins : le champ se relie à chaque frappe, et il reconstruirait la
|
||||
/// liste entière à « non précisé ». Le rapprochement se fait sur la forme normalisée du nom,
|
||||
/// pour survivre à un accent ajouté après coup.
|
||||
/// </remarks>
|
||||
public static List<AuteurSaisi> Decouper(string? saisie, IEnumerable<AuteurSaisi>? connus = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saisie))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var roles = new Dictionary<string, RoleAuteur>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var connu in connus ?? [])
|
||||
{
|
||||
roles.TryAdd(NormalisationTexte.Normaliser(connu.Nom), connu.Role);
|
||||
}
|
||||
|
||||
return saisie
|
||||
.Split(SeparateurAuteurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(nom => new AuteurSaisi(
|
||||
nom, roles.GetValueOrDefault(NormalisationTexte.Normaliser(nom), RoleAuteur.NonPrecise)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Un auteur tel qu'il est saisi : son nom, et son rôle sur ce livre.</summary>
|
||||
/// <remarks>
|
||||
/// Le rôle n'appartient pas à la personne mais au <b>lien</b> : le même auteur scénarise un
|
||||
/// album et dessine le suivant. Voir <see cref="RoleAuteur"/>.
|
||||
/// </remarks>
|
||||
public record AuteurSaisi(string Nom, RoleAuteur Role = RoleAuteur.NonPrecise);
|
||||
|
||||
/// <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 »,
|
||||
|
||||
@@ -12,6 +12,13 @@ public record AuteurDto
|
||||
|
||||
/// <summary>Nombre de livres du catalogue rattachés à cet auteur. Renseigné par la liste des auteurs.</summary>
|
||||
public int NombreDeLivres { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rôle sur le livre où cet auteur figure. <b>N'a de sens qu'à l'intérieur d'un
|
||||
/// <see cref="LivreDto"/></b> : la liste des auteurs, elle, ne parle d'aucun livre en
|
||||
/// particulier et le laisse à <c>NonPrecise</c>.
|
||||
/// </summary>
|
||||
public RoleAuteur Role { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -60,6 +60,15 @@ public class LivreAuteur
|
||||
|
||||
/// <summary>Rang de l'auteur sur la couverture, à partir de 0.</summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rôle sur <b>ce livre</b> : scénario, dessin, couleurs. Voir <see cref="RoleAuteur"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Il est porté par le lien et non par l'auteur : le même dessinateur peut avoir scénarisé
|
||||
/// un autre album, et il ne doit pas pour autant exister en deux fiches.
|
||||
/// </remarks>
|
||||
public RoleAuteur Role { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace MaBibli.Shared.Entites;
|
||||
|
||||
/// <summary>
|
||||
/// Rôle d'un auteur <b>sur un livre donné</b>, et non une propriété de la personne.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le même auteur scénarise un album et dessine le suivant : le rôle vit donc sur le lien
|
||||
/// <see cref="LivreAuteur"/>, jamais sur <see cref="Auteur"/>. C'est ce qui permet aussi de
|
||||
/// garder <b>une seule fiche</b> par personne, à laquelle tous ses livres se rattachent.
|
||||
/// <para>
|
||||
/// ⚠️ <see cref="NonPrecise"/> est la valeur par défaut, et ne veut pas dire « auteur du
|
||||
/// texte » : la quasi-totalité des livres n'a qu'un signataire dont le rôle ne se pose pas.
|
||||
/// Même règle que <see cref="TypeDocument"/> — l'interface n'affiche que ce qui a été choisi.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Liste volontairement courte : le besoin remonté est la <b>bande dessinée</b>, où l'on veut
|
||||
/// savoir qui écrit et qui dessine. Une valeur s'ajoute sans migration, la renommer une fois
|
||||
/// posée sur des centaines de liens, non.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum RoleAuteur
|
||||
{
|
||||
/// <summary>Aucun rôle particulier — le cas de l'immense majorité des livres.</summary>
|
||||
NonPrecise = 0,
|
||||
|
||||
Scenario = 1,
|
||||
|
||||
Dessin = 2,
|
||||
|
||||
/// <summary>Mise en couleurs : un troisième nom figure souvent sur un album.</summary>
|
||||
Couleurs = 3,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Entites;
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
@@ -117,6 +119,41 @@ public class RapprochementAuteursTests
|
||||
public void La_normalisation_met_a_plat_casse_accents_et_ponctuation(string? brut, string attendu)
|
||||
=> Assert.Equal(attendu, NormalisationTexte.Normaliser(brut));
|
||||
|
||||
/// <summary>
|
||||
/// Retaper le champ des noms ne doit pas effacer les rôles déjà posés.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le champ se relie à chaque frappe : sans reprise des rôles connus, corriger une faute de
|
||||
/// frappe dans le nom d'un auteur remettrait tous ses voisins à « non précisé », sous les
|
||||
/// yeux de l'utilisateur et sans qu'il comprenne pourquoi.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void Redecouper_la_ligne_dauteurs_preserve_les_roles()
|
||||
{
|
||||
var saisie = new EnregistrementLivre
|
||||
{
|
||||
Auteurs =
|
||||
[
|
||||
new AuteurSaisi("Dobbs", RoleAuteur.Scenario),
|
||||
new AuteurSaisi("Christophe", RoleAuteur.Dessin),
|
||||
],
|
||||
};
|
||||
|
||||
// L'utilisateur ajoute un accent au second nom : la ligne est redécoupée entièrement.
|
||||
saisie.Auteur = "Dobbs; Christophé";
|
||||
|
||||
// Les deux rôles survivent : le rapprochement se fait sur la forme NORMALISÉE, donc un
|
||||
// accent ajouté après coup ne casse rien.
|
||||
Assert.Equal(
|
||||
[RoleAuteur.Scenario, RoleAuteur.Dessin],
|
||||
saisie.Auteurs.Select(a => a.Role));
|
||||
|
||||
// Un nom réellement différent, lui, n'est pas reconnu : son rôle repart à zéro, ce qui
|
||||
// est honnête — on ne devine pas qu'il s'agit de la même personne.
|
||||
saisie.Auteur = "Dobbs; Marchand";
|
||||
Assert.Equal(RoleAuteur.NonPrecise, saisie.Auteurs[1].Role);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La ponctuation devient une espace et ne disparaît pas : sans ça « P.F. » donnerait un seul
|
||||
/// mot et les initiales ne pourraient plus s'aligner.
|
||||
|
||||
@@ -390,6 +390,74 @@ public class ServiceCatalogueTests : IDisposable
|
||||
Assert.Equal(["Germinal"], nonPrecises.Select(l => l.Titre));
|
||||
}
|
||||
|
||||
// ── Rôles des auteurs : ils vivent sur le LIEN, pas sur la personne ──────
|
||||
|
||||
[Fact]
|
||||
public async Task Les_roles_sont_enregistres_par_livre_et_pas_par_personne()
|
||||
{
|
||||
var bd = Saisie("Le Horla", format: Format.Physique);
|
||||
bd.TypeDocument = TypeDocument.BandeDessinee;
|
||||
bd.Auteurs =
|
||||
[
|
||||
new AuteurSaisi("Guy de Maupassant", RoleAuteur.Scenario),
|
||||
new AuteurSaisi("Guillaume Sorel", RoleAuteur.Dessin),
|
||||
];
|
||||
|
||||
var premier = (await _service.CreerAsync(bd, "mathieu")).Livre!;
|
||||
|
||||
// Le MÊME dessinateur, scénariste ailleurs : une seule fiche d'auteur, deux rôles.
|
||||
var autre = Saisie("Autre album");
|
||||
autre.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Scenario)];
|
||||
var second = (await _service.CreerAsync(autre, "mathieu", confirmerDoublon: true)).Livre!;
|
||||
|
||||
Assert.Equal(
|
||||
[RoleAuteur.Scenario, RoleAuteur.Dessin],
|
||||
premier.Auteurs.Select(a => a.Role));
|
||||
|
||||
Assert.Equal(RoleAuteur.Scenario, second.Auteurs.Single().Role);
|
||||
Assert.Equal(2, await _db.Auteurs.CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_livre_sans_role_precise_reste_non_precise()
|
||||
{
|
||||
var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
||||
|
||||
Assert.Equal(RoleAuteur.NonPrecise, livre.Auteurs.Single().Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Le_role_suit_lauteur_meme_si_son_nom_est_ecrit_autrement()
|
||||
{
|
||||
var saisie = Saisie("Le Horla");
|
||||
saisie.Auteurs =
|
||||
[
|
||||
// Variante sûre d'un auteur déjà connu : le serveur la rapproche, et le rôle doit
|
||||
// suivre la fiche RÉSOLUE, pas la position dans la saisie.
|
||||
new AuteurSaisi("Maupassant, Guy de", RoleAuteur.Scenario),
|
||||
];
|
||||
|
||||
await _service.CreerAsync(Saisie("Bel-Ami", "Guy de Maupassant"), "mathieu");
|
||||
var livre = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
|
||||
|
||||
Assert.Single(await _db.Auteurs.ToListAsync());
|
||||
Assert.Equal(RoleAuteur.Scenario, livre.Auteurs.Single().Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Modifier_un_livre_change_le_role_sans_toucher_a_lauteur()
|
||||
{
|
||||
var saisie = Saisie("Le Horla");
|
||||
saisie.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Dessin)];
|
||||
var cree = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
|
||||
|
||||
saisie.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Couleurs)];
|
||||
var modifie = (await _service.ModifierAsync(cree.Id, saisie, "mathieu")).Livre!;
|
||||
|
||||
Assert.Equal(RoleAuteur.Couleurs, modifie.Auteurs.Single().Role);
|
||||
Assert.Single(await _db.Auteurs.ToListAsync());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Doublons : un AVERTISSEMENT, jamais un refus. Posséder deux exemplaires est légitime —
|
||||
// on garde le sien et on prête l'autre — et l'ISBN est facultatif, donc il ne peut pas
|
||||
|
||||
Reference in New Issue
Block a user