Transforme la bibliographie en écran de travail D1 à D5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
ddc60898da
commit
359f4f59c6
@@ -1934,3 +1934,26 @@ Depuis une place manquante, un ISBN passe par la cascade BnF puis OpenLibrary ex
|
||||
candidat choisi peut créer un livre du catalogue puis rattacher la place, ou créer une envie avec
|
||||
les métadonnées complètes, notamment l'auteur trouvé par le lookup. Les revues (préfixe `977`) ne
|
||||
font pas partie de ce flux : `ElementSerie.LivreId` ne pointe que vers `Livre`.
|
||||
|
||||
## Lot D — bibliographie écran de travail (2026-08-20)
|
||||
|
||||
La bibliographie conserve maintenant les résultats réussis pendant la session du client. Ce cache
|
||||
est volontairement **mémoire uniquement**, par auteur et par instance de `ServiceLivresApi` :
|
||||
une bibliographie dépend de la réponse BnF et des souhaits personnels, et n'est pas un instantané
|
||||
de la bibliothèque. Une réponse `EtatSourceBibliographie.Ok` remplace toujours l'entrée ; une
|
||||
réponse `SourceMuette` n'est jamais mise en cache et ne consulte jamais une entrée précédente.
|
||||
Ainsi un incident BnF ne peut pas figer une bibliographie vide.
|
||||
|
||||
L'écran possède un filtre replié derrière « Filtrer », avec compteur, distinguant tout le fonds,
|
||||
les œuvres possédées et celles à découvrir. Les œuvres possédées réutilisent `CarteLivre` et
|
||||
affichent donc le statut de lecture personnel déjà fourni par `LivreDto`.
|
||||
|
||||
Le masquage est une décision **personnelle**, comme la liste d'envies : il exprime qu'une œuvre
|
||||
n'est pas pertinente pour le lecteur courant, et ne doit pas imposer ce choix au foyer. La table
|
||||
`BibliographiesMasquees` est indexée par utilisateur, auteur et clé d'œuvre ; le compteur visible
|
||||
et le filtre « Afficher les masquées » empêchent tout masquage silencieux. Masquer est une
|
||||
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.
|
||||
|
||||
@@ -187,6 +187,8 @@ Le lot le plus structurant, et le seul qui touche au modèle.
|
||||
|
||||
## Lot D — La bibliographie devient un écran de travail
|
||||
|
||||
✅ Traité le 2026-08-20 : voir `CLAUDE.md`, section « Lot D — bibliographie écran de travail ».
|
||||
|
||||
Aujourd'hui elle se consulte ; les cinq items la font servir à **constituer** une liste.
|
||||
|
||||
- **D1. Mettre en cache une bibliographie déjà demandée.** Elle coûte ~0,85 s par page, deux
|
||||
|
||||
@@ -24,6 +24,8 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
/// </summary>
|
||||
public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();
|
||||
|
||||
public DbSet<BibliographieMasquee> BibliographiesMasquees => Set<BibliographieMasquee>();
|
||||
|
||||
/// <summary>
|
||||
/// Sagas, cycles et séries. <b>Communs au foyer</b>, comme le catalogue : l'ordre de lecture
|
||||
/// est une propriété de l'œuvre, pas du lecteur.
|
||||
@@ -137,6 +139,17 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<BibliographieMasquee>(masquee =>
|
||||
{
|
||||
masquee.Property(m => m.Utilisateur).IsRequired();
|
||||
masquee.Property(m => m.TitreNormalise).IsRequired();
|
||||
masquee.HasIndex(
|
||||
m => new { m.AuteurId, m.Utilisateur, m.TitreNormalise },
|
||||
"IX_BibliographiesMasquees_Auteur_Utilisateur_Titre")
|
||||
.IsUnique();
|
||||
masquee.HasIndex(m => new { m.Utilisateur, m.AuteurId });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Serie>(serie =>
|
||||
{
|
||||
serie.Property(s => s.Titre).IsRequired();
|
||||
|
||||
+528
@@ -0,0 +1,528 @@
|
||||
// <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("20260820113543_MasquageBibliographie")]
|
||||
partial class MasquageBibliographie
|
||||
{
|
||||
/// <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.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,47 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MaBibli.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MasquageBibliographie : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BibliographiesMasquees",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AuteurId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Utilisateur = table.Column<string>(type: "TEXT", nullable: false),
|
||||
TitreNormalise = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BibliographiesMasquees", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BibliographiesMasquees_Auteur_Utilisateur_Titre",
|
||||
table: "BibliographiesMasquees",
|
||||
columns: new[] { "AuteurId", "Utilisateur", "TitreNormalise" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BibliographiesMasquees_Utilisateur_AuteurId",
|
||||
table: "BibliographiesMasquees",
|
||||
columns: new[] { "Utilisateur", "AuteurId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BibliographiesMasquees");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,33 @@ namespace MaBibli.Api.Data.Migrations
|
||||
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")
|
||||
|
||||
@@ -180,6 +180,37 @@ public static class SouhaitsEndpoints
|
||||
.Produces<BibliographieDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
routes.MapPost("/api/auteurs/{id:int}/bibliographie/masque", async (
|
||||
int id,
|
||||
MasquageBibliographie saisie,
|
||||
IServiceBibliographie service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var fait = await service.MasquerAsync(
|
||||
id, saisie.Titre, utilisateurs.Obtenir().Identifiant, ct);
|
||||
return fait ? Results.NoContent() : Results.BadRequest();
|
||||
})
|
||||
.WithName("MasquerOeuvreBibliographie")
|
||||
.WithSummary("Masque une œuvre de la bibliographie pour l'appelant.")
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
routes.MapDelete("/api/auteurs/{id:int}/bibliographie/masque", async (
|
||||
int id,
|
||||
string titre,
|
||||
IServiceBibliographie service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var fait = await service.DemasquerAsync(
|
||||
id, titre, utilisateurs.Obtenir().Identifiant, ct);
|
||||
return fait ? Results.NoContent() : Results.NotFound();
|
||||
})
|
||||
.WithName("DemasquerOeuvreBibliographie")
|
||||
.WithSummary("Réaffiche une œuvre masquée de la bibliographie.")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MaBibli.Api.Data;
|
||||
using MaBibli.Api.Services.Isbn;
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Entites;
|
||||
using MaBibli.Shared.Textes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -13,6 +14,10 @@ public interface IServiceBibliographie
|
||||
/// l'appelant souhaite déjà. <c>null</c> si l'auteur n'existe pas.
|
||||
/// </summary>
|
||||
Task<BibliographieDto?> ObtenirAsync(int auteurId, string? utilisateur, CancellationToken ct = default);
|
||||
|
||||
Task<bool> MasquerAsync(int auteurId, string titre, string? utilisateur, CancellationToken ct = default);
|
||||
|
||||
Task<bool> DemasquerAsync(int auteurId, string titre, string? utilisateur, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,9 +78,10 @@ public sealed class ServiceBibliographie(
|
||||
}
|
||||
|
||||
var souhaits = await SouhaitsAsync(utilisateur, ct);
|
||||
var masquees = await MasqueesAsync(auteurId, utilisateur, ct);
|
||||
|
||||
var oeuvres = Regrouper(resultat)
|
||||
.Select(o => Confronter(o, parTitre, parIsbn, souhaits))
|
||||
.Select(o => Confronter(o, parTitre, parIsbn, souhaits, masquees))
|
||||
.OrderByDescending(o => AnneeTriable(o.Annee))
|
||||
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
@@ -95,6 +101,58 @@ public sealed class ServiceBibliographie(
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> MasquerAsync(
|
||||
int auteurId, string titre, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cle = CleOeuvre.Cle(titre);
|
||||
if (cle.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var existe = await db.BibliographiesMasquees.AnyAsync(
|
||||
m => m.AuteurId == auteurId && m.Utilisateur == utilisateur && m.TitreNormalise == cle, ct);
|
||||
if (existe)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
db.BibliographiesMasquees.Add(new BibliographieMasquee
|
||||
{
|
||||
AuteurId = auteurId,
|
||||
Utilisateur = utilisateur,
|
||||
TitreNormalise = cle,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DemasquerAsync(
|
||||
int auteurId, string titre, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cle = CleOeuvre.Cle(titre);
|
||||
var ligne = await db.BibliographiesMasquees.FirstOrDefaultAsync(
|
||||
m => m.AuteurId == auteurId && m.Utilisateur == utilisateur && m.TitreNormalise == cle, ct);
|
||||
if (ligne is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
db.BibliographiesMasquees.Remove(ligne);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Clés d'œuvre déjà souhaitées par l'appelant, avec l'identifiant de l'envie.</summary>
|
||||
private async Task<Dictionary<string, int>> SouhaitsAsync(string? utilisateur, CancellationToken ct)
|
||||
{
|
||||
@@ -118,6 +176,22 @@ public sealed class ServiceBibliographie(
|
||||
return index;
|
||||
}
|
||||
|
||||
private async Task<HashSet<string>> MasqueesAsync(
|
||||
int auteurId, string? utilisateur, CancellationToken ct)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var cles = await db.BibliographiesMasquees
|
||||
.AsNoTracking()
|
||||
.Where(m => m.AuteurId == auteurId && m.Utilisateur == utilisateur)
|
||||
.Select(m => m.TitreNormalise)
|
||||
.ToListAsync(ct);
|
||||
return cles.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Réunit les rééditions sous une seule œuvre.
|
||||
/// </summary>
|
||||
@@ -158,7 +232,8 @@ public sealed class ServiceBibliographie(
|
||||
OeuvreBibliographie oeuvre,
|
||||
IReadOnlyDictionary<string, int> parTitre,
|
||||
IReadOnlyDictionary<string, int> parIsbn,
|
||||
IReadOnlyDictionary<string, int> souhaits)
|
||||
IReadOnlyDictionary<string, int> souhaits,
|
||||
IReadOnlySet<string> masquees)
|
||||
{
|
||||
var cle = CleOeuvre.Cle(oeuvre.Titre);
|
||||
|
||||
@@ -179,6 +254,7 @@ public sealed class ServiceBibliographie(
|
||||
LivreId = livreId,
|
||||
Souhaite = souhaitId is not null,
|
||||
SouhaitId = souhaitId,
|
||||
Masquee = masquees.Contains(cle),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ else if (_bibliographie is { } biblio)
|
||||
<p class="compteur">
|
||||
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
|
||||
— @_possedees déjà chez vous
|
||||
@if (_masquees > 0)
|
||||
{
|
||||
<text> — @_masquees masquée@(_masquees > 1 ? "s" : "")</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
@* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments
|
||||
@@ -124,16 +128,37 @@ else if (_bibliographie is { } biblio)
|
||||
<p class="message-erreur" role="alert">@_message</p>
|
||||
}
|
||||
|
||||
@* Filtre sur une seule question : « qu'est-ce qui me manque ? ». Les livres possédés ne
|
||||
sont jamais masqués par défaut — le grisage est l'information principale de l'écran. *@
|
||||
<div class="filtres">
|
||||
<div class="segments" role="group" aria-label="Filtrer la bibliographie">
|
||||
<button type="button" class="segment @(_seulementManquants ? null : "segment-actif")"
|
||||
@onclick="() => _seulementManquants = false">Tout</button>
|
||||
<button type="button" class="segment @(_seulementManquants ? "segment-actif" : null)"
|
||||
@onclick="() => _seulementManquants = true">Ce qui me manque</button>
|
||||
</div>
|
||||
<div class="actions-formulaire">
|
||||
<button type="button" class="bouton bouton-discret"
|
||||
@onclick="() => _filtresOuverts = !_filtresOuverts"
|
||||
aria-expanded="@_filtresOuverts">
|
||||
Filtrer (@NombreFiltres)
|
||||
</button>
|
||||
</div>
|
||||
@if (_filtresOuverts)
|
||||
{
|
||||
<div class="filtres">
|
||||
<div class="segments" role="group" aria-label="Possession">
|
||||
<button type="button" class="segment @(_modePossession == 0 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 0">Tout</button>
|
||||
<button type="button" class="segment @(_modePossession == 1 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 1">Ceux que j'ai déjà</button>
|
||||
<button type="button" class="segment @(_modePossession == 2 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 2">Ce qui me manque</button>
|
||||
</div>
|
||||
@if (_masquees > 0)
|
||||
{
|
||||
<div class="segments">
|
||||
<button type="button" class="segment @(_afficherMasquees ? "segment-actif" : null)"
|
||||
@onclick="() => _afficherMasquees = true">
|
||||
Afficher les masquées (@_masquees)
|
||||
</button>
|
||||
<button type="button" class="segment @(!_afficherMasquees ? "segment-actif" : null)"
|
||||
@onclick="() => _afficherMasquees = false">Masquer les masquées</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ul class="liste-bibliographie">
|
||||
@foreach (var oeuvre in Affichees(biblio))
|
||||
@@ -204,16 +229,51 @@ else if (_bibliographie is { } biblio)
|
||||
|
||||
@if (oeuvre.ADecouvrir)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
|
||||
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
|
||||
Je le veux
|
||||
<div class="actions-formulaire">
|
||||
<label class="selection-oeuvre">
|
||||
<input type="checkbox"
|
||||
checked="@_selection.Contains(oeuvre.Titre)"
|
||||
@onchange="e => Selectionner(oeuvre.Titre, e.Value is true)" />
|
||||
Sélectionner
|
||||
</label>
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
|
||||
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
|
||||
Je le veux
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!oeuvre.Masquee)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@_enCours" @onclick="() => MasquerAsync(oeuvre)">
|
||||
Masquer
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@_enCours" @onclick="() => DemasquerAsync(oeuvre)">
|
||||
Réafficher
|
||||
</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@if (_selection.Count > 0)
|
||||
{
|
||||
<div class="actions-flottantes">
|
||||
<button type="button" class="bouton bouton-principal"
|
||||
disabled="@(_enCours || !Reseau.EnLigne)"
|
||||
title="@(Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne)"
|
||||
@onclick="AjouterSelectionAsync">
|
||||
Ajouter la sélection aux envies (@_selection.Count)
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Le rapprochement est fait par titre, et il rate des choses (voir CleOeuvre). Le dire
|
||||
franchement vaut mieux que laisser l'utilisateur découvrir seul qu'un livre qu'il
|
||||
possède n'est pas grisé. *@
|
||||
@@ -239,7 +299,9 @@ else if (_bibliographie is { } biblio)
|
||||
public int AuteurId { get; set; }
|
||||
|
||||
private BibliographieDto? _bibliographie;
|
||||
private bool _seulementManquants;
|
||||
private int _modePossession;
|
||||
private bool _filtresOuverts;
|
||||
private bool _afficherMasquees;
|
||||
private bool _enCours;
|
||||
private bool _chargement;
|
||||
|
||||
@@ -250,13 +312,22 @@ else if (_bibliographie is { } biblio)
|
||||
private string? _message;
|
||||
|
||||
private int _possedees;
|
||||
private int _masquees;
|
||||
private IReadOnlyList<LivreDto> _livresCatalogue = [];
|
||||
private readonly HashSet<string> _oeuvresOuvertes = [];
|
||||
private readonly HashSet<string> _selection = [];
|
||||
|
||||
protected override async Task OnParametersSetAsync() => await ChargerAsync();
|
||||
|
||||
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
|
||||
_seulementManquants ? biblio.Oeuvres.Where(o => o.ADecouvrir) : biblio.Oeuvres;
|
||||
biblio.Oeuvres.Where(o =>
|
||||
(_afficherMasquees || !o.Masquee)
|
||||
&& (_modePossession == 0
|
||||
|| (_modePossession == 1 && o.Possede)
|
||||
|| (_modePossession == 2 && o.ADecouvrir)));
|
||||
|
||||
private int NombreFiltres =>
|
||||
(_modePossession == 0 ? 0 : 1) + (_masquees > 0 && !_afficherMasquees ? 1 : 0);
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
@@ -278,6 +349,7 @@ else if (_bibliographie is { } biblio)
|
||||
}
|
||||
|
||||
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
|
||||
_masquees = _bibliographie.Oeuvres.Count(o => o.Masquee);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -300,6 +372,70 @@ else if (_bibliographie is { } biblio)
|
||||
}
|
||||
}
|
||||
|
||||
private void Selectionner(string titre, bool selectionnee)
|
||||
{
|
||||
if (selectionnee)
|
||||
{
|
||||
_selection.Add(titre);
|
||||
}
|
||||
else
|
||||
{
|
||||
_selection.Remove(titre);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AjouterSelectionAsync()
|
||||
{
|
||||
if (_bibliographie is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var saisies = _bibliographie.Oeuvres
|
||||
.Where(o => _selection.Contains(o.Titre) && o.ADecouvrir)
|
||||
.Select(o => new EnregistrementSouhait
|
||||
{
|
||||
Titre = o.Titre,
|
||||
Auteur = _bibliographie.Auteur.Nom,
|
||||
Editeur = o.Editeur,
|
||||
Annee = o.Annee,
|
||||
Isbn = o.Isbn,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_enCours = true;
|
||||
_message = null;
|
||||
var resultats = await Api.AjouterSouhaitsAsync(saisies);
|
||||
_enCours = false;
|
||||
var erreurs = resultats.Where(r => !r.EstOk).Select(r => r.Erreur).Where(e => e is not null).ToList();
|
||||
_message = erreurs.Count == 0 ? null : string.Join(" ", erreurs);
|
||||
_selection.Clear();
|
||||
await ChargerAsync();
|
||||
}
|
||||
|
||||
private async Task MasquerAsync(OeuvreBibliographie oeuvre)
|
||||
{
|
||||
await ModifierMasquageAsync(oeuvre, false);
|
||||
}
|
||||
|
||||
private async Task DemasquerAsync(OeuvreBibliographie oeuvre)
|
||||
{
|
||||
await ModifierMasquageAsync(oeuvre, true);
|
||||
}
|
||||
|
||||
private async Task ModifierMasquageAsync(OeuvreBibliographie oeuvre, bool demasquer)
|
||||
{
|
||||
_enCours = true;
|
||||
_message = demasquer
|
||||
? await Api.DemasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre)
|
||||
: await Api.MasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre);
|
||||
_enCours = false;
|
||||
if (_message is null)
|
||||
{
|
||||
await ChargerAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void BasculerOeuvre(string titre)
|
||||
{
|
||||
if (!_oeuvresOuvertes.Add(titre))
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace MaBibli.Client.Services;
|
||||
public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, EtatReseau reseau)
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||
private readonly Dictionary<int, BibliographieDto> _bibliographies = [];
|
||||
|
||||
/// <summary>
|
||||
/// Recharge tous les instantanés depuis le réseau.
|
||||
@@ -615,8 +616,29 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
}
|
||||
|
||||
public async Task<ResultatApi<SouhaitDto>> AjouterSouhaitAsync(
|
||||
EnregistrementSouhait saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<SouhaitDto>(() => http.PostAsJsonAsync("api/souhaits", saisie, Json, ct), ct);
|
||||
EnregistrementSouhait saisie, CancellationToken ct = default)
|
||||
{
|
||||
var resultat = await EcrireAsync<SouhaitDto>(
|
||||
() => http.PostAsJsonAsync("api/souhaits", saisie, Json, ct), ct);
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
_bibliographies.Clear();
|
||||
}
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ResultatApi<SouhaitDto>>> AjouterSouhaitsAsync(
|
||||
IReadOnlyList<EnregistrementSouhait> saisies, CancellationToken ct = default)
|
||||
{
|
||||
var resultats = new List<ResultatApi<SouhaitDto>>(saisies.Count);
|
||||
foreach (var saisie in saisies)
|
||||
{
|
||||
resultats.Add(await AjouterSouhaitAsync(saisie, ct));
|
||||
}
|
||||
|
||||
return resultats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retire une envie. <c>null</c> si c'est fait, sinon le motif à afficher.
|
||||
@@ -735,10 +757,48 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
public async Task<BibliographieDto?> ObtenirBibliographieAsync(
|
||||
int auteurId, CancellationToken ct = default)
|
||||
{
|
||||
if (_bibliographies.TryGetValue(auteurId, out var cachee)
|
||||
&& cachee.Etat == EtatSourceBibliographie.Ok)
|
||||
{
|
||||
return cachee;
|
||||
}
|
||||
|
||||
var reponse = await http.GetAsync($"api/auteurs/{auteurId}/bibliographie", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
var bibliographie = reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<BibliographieDto>(reponse, ct);
|
||||
if (bibliographie?.Etat == EtatSourceBibliographie.Ok)
|
||||
{
|
||||
_bibliographies[auteurId] = bibliographie;
|
||||
}
|
||||
|
||||
return bibliographie;
|
||||
}
|
||||
|
||||
public async Task<string?> MasquerOeuvreBibliographieAsync(
|
||||
int auteurId, string titre, CancellationToken ct = default) =>
|
||||
await ModifierBibliographieAsync(
|
||||
() => http.PostAsJsonAsync(
|
||||
$"api/auteurs/{auteurId}/bibliographie/masque",
|
||||
new MasquageBibliographie { Titre = titre }, Json, ct), ct);
|
||||
|
||||
public async Task<string?> DemasquerOeuvreBibliographieAsync(
|
||||
int auteurId, string titre, CancellationToken ct = default) =>
|
||||
await ModifierBibliographieAsync(
|
||||
() => http.DeleteAsync(
|
||||
$"api/auteurs/{auteurId}/bibliographie/masque?titre={Uri.EscapeDataString(titre)}"), ct);
|
||||
|
||||
private async Task<string?> ModifierBibliographieAsync(
|
||||
Func<Task<HttpResponseMessage>> appel, CancellationToken ct)
|
||||
{
|
||||
var (reponse, motif) = await EnvoyerAsync(appel);
|
||||
if (reponse is null)
|
||||
{
|
||||
return motif;
|
||||
}
|
||||
|
||||
_bibliographies.Clear();
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -68,6 +68,12 @@ public record EnregistrementSouhait
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Charge utile du masquage personnel d'une œuvre bibliographique.</summary>
|
||||
public record MasquageBibliographie
|
||||
{
|
||||
public string Titre { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Formats d'export de la liste d'envies.</summary>
|
||||
/// <remarks>
|
||||
/// Les deux usages décrits dans IDEES.md ne demandent pas la même chose : « l'emporter en
|
||||
@@ -116,6 +122,9 @@ public record OeuvreBibliographie
|
||||
|
||||
public int? SouhaitId { get; init; }
|
||||
|
||||
/// <summary>Vrai si l'appelant a explicitement masqué cette œuvre.</summary>
|
||||
public bool Masquee { get; init; }
|
||||
|
||||
/// <summary>Ni possédé, ni déjà souhaité : ce qui reste à découvrir.</summary>
|
||||
[JsonIgnore]
|
||||
public bool ADecouvrir => !Possede && !Souhaite;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Shared.Entites;
|
||||
|
||||
/// <summary>Une œuvre explicitement masquée par un utilisateur dans une bibliographie.</summary>
|
||||
/// <remarks>
|
||||
/// Le masquage est personnel : il exprime que cette œuvre n'est pas pertinente pour ce lecteur,
|
||||
/// pas qu'elle ne l'est pour le foyer. La clé d'œuvre reste celle de la bibliographie BnF.
|
||||
/// </remarks>
|
||||
public class BibliographieMasquee
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int AuteurId { get; set; }
|
||||
|
||||
public string Utilisateur { get; set; } = string.Empty;
|
||||
|
||||
public string TitreNormalise { get; set; } = string.Empty;
|
||||
|
||||
public static BibliographieMasquee Creer(int auteurId, string utilisateur, string titre) => new()
|
||||
{
|
||||
AuteurId = auteurId,
|
||||
Utilisateur = utilisateur,
|
||||
TitreNormalise = CleOeuvre.Cle(titre),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MaBibli.Api.Data;
|
||||
using MaBibli.Api.Services.Catalogue;
|
||||
using MaBibli.Shared.Textes;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
public sealed class ServiceBibliographieMasquageTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connexion = new("Data Source=:memory:");
|
||||
private readonly MaBibliDbContext _db;
|
||||
private readonly ServiceBibliographie _service;
|
||||
|
||||
public ServiceBibliographieMasquageTests()
|
||||
{
|
||||
_connexion.Open();
|
||||
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
|
||||
.UseSqlite(_connexion)
|
||||
.Options;
|
||||
_db = new MaBibliDbContext(options);
|
||||
_db.Database.EnsureCreated();
|
||||
_service = new ServiceBibliographie(_db, null!, new ServiceAuteurs(_db));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Le_masquage_est_personnel_et_idempotent()
|
||||
{
|
||||
const int auteurId = 42;
|
||||
|
||||
Assert.True(await _service.MasquerAsync(auteurId, "L'Œuvre", "mathieu"));
|
||||
Assert.True(await _service.MasquerAsync(auteurId, "L'oeuvre", "mathieu"));
|
||||
|
||||
var ligne = await _db.BibliographiesMasquees.SingleAsync();
|
||||
Assert.Equal(CleOeuvre.Cle("L'Œuvre"), ligne.TitreNormalise);
|
||||
Assert.False(await _service.DemasquerAsync(auteurId, "L'Œuvre", "camille"));
|
||||
Assert.True(await _service.DemasquerAsync(auteurId, "L'oeuvre", "mathieu"));
|
||||
Assert.Empty(await _db.BibliographiesMasquees.ToListAsync());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
_connexion.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user