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
|
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
|
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`.
|
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
|
## 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.
|
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
|
- **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>
|
/// </summary>
|
||||||
public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();
|
public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();
|
||||||
|
|
||||||
|
public DbSet<BibliographieMasquee> BibliographiesMasquees => Set<BibliographieMasquee>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sagas, cycles et séries. <b>Communs au foyer</b>, comme le catalogue : l'ordre de lecture
|
/// 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.
|
/// est une propriété de l'œuvre, pas du lecteur.
|
||||||
@@ -137,6 +139,17 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
|||||||
.IsUnique();
|
.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 =>
|
modelBuilder.Entity<Serie>(serie =>
|
||||||
{
|
{
|
||||||
serie.Property(s => s.Titre).IsRequired();
|
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");
|
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 =>
|
modelBuilder.Entity("MaBibli.Shared.Entites.ElementSerie", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -180,6 +180,37 @@ public static class SouhaitsEndpoints
|
|||||||
.Produces<BibliographieDto>()
|
.Produces<BibliographieDto>()
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.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;
|
return routes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using MaBibli.Api.Data;
|
using MaBibli.Api.Data;
|
||||||
using MaBibli.Api.Services.Isbn;
|
using MaBibli.Api.Services.Isbn;
|
||||||
using MaBibli.Shared.Dtos;
|
using MaBibli.Shared.Dtos;
|
||||||
|
using MaBibli.Shared.Entites;
|
||||||
using MaBibli.Shared.Textes;
|
using MaBibli.Shared.Textes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -13,6 +14,10 @@ public interface IServiceBibliographie
|
|||||||
/// l'appelant souhaite déjà. <c>null</c> si l'auteur n'existe pas.
|
/// l'appelant souhaite déjà. <c>null</c> si l'auteur n'existe pas.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<BibliographieDto?> ObtenirAsync(int auteurId, string? utilisateur, CancellationToken ct = default);
|
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>
|
/// <summary>
|
||||||
@@ -73,9 +78,10 @@ public sealed class ServiceBibliographie(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var souhaits = await SouhaitsAsync(utilisateur, ct);
|
var souhaits = await SouhaitsAsync(utilisateur, ct);
|
||||||
|
var masquees = await MasqueesAsync(auteurId, utilisateur, ct);
|
||||||
|
|
||||||
var oeuvres = Regrouper(resultat)
|
var oeuvres = Regrouper(resultat)
|
||||||
.Select(o => Confronter(o, parTitre, parIsbn, souhaits))
|
.Select(o => Confronter(o, parTitre, parIsbn, souhaits, masquees))
|
||||||
.OrderByDescending(o => AnneeTriable(o.Annee))
|
.OrderByDescending(o => AnneeTriable(o.Annee))
|
||||||
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
|
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
|
||||||
.ToList();
|
.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>
|
/// <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)
|
private async Task<Dictionary<string, int>> SouhaitsAsync(string? utilisateur, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -118,6 +176,22 @@ public sealed class ServiceBibliographie(
|
|||||||
return index;
|
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>
|
/// <summary>
|
||||||
/// Réunit les rééditions sous une seule œuvre.
|
/// Réunit les rééditions sous une seule œuvre.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -158,7 +232,8 @@ public sealed class ServiceBibliographie(
|
|||||||
OeuvreBibliographie oeuvre,
|
OeuvreBibliographie oeuvre,
|
||||||
IReadOnlyDictionary<string, int> parTitre,
|
IReadOnlyDictionary<string, int> parTitre,
|
||||||
IReadOnlyDictionary<string, int> parIsbn,
|
IReadOnlyDictionary<string, int> parIsbn,
|
||||||
IReadOnlyDictionary<string, int> souhaits)
|
IReadOnlyDictionary<string, int> souhaits,
|
||||||
|
IReadOnlySet<string> masquees)
|
||||||
{
|
{
|
||||||
var cle = CleOeuvre.Cle(oeuvre.Titre);
|
var cle = CleOeuvre.Cle(oeuvre.Titre);
|
||||||
|
|
||||||
@@ -179,6 +254,7 @@ public sealed class ServiceBibliographie(
|
|||||||
LivreId = livreId,
|
LivreId = livreId,
|
||||||
Souhaite = souhaitId is not null,
|
Souhaite = souhaitId is not null,
|
||||||
SouhaitId = souhaitId,
|
SouhaitId = souhaitId,
|
||||||
|
Masquee = masquees.Contains(cle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ else if (_bibliographie is { } biblio)
|
|||||||
<p class="compteur">
|
<p class="compteur">
|
||||||
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
|
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
|
||||||
— @_possedees déjà chez vous
|
— @_possedees déjà chez vous
|
||||||
|
@if (_masquees > 0)
|
||||||
|
{
|
||||||
|
<text> — @_masquees masquée@(_masquees > 1 ? "s" : "")</text>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments
|
@* 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>
|
<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
|
<div class="actions-formulaire">
|
||||||
sont jamais masqués par défaut — le grisage est l'information principale de l'écran. *@
|
<button type="button" class="bouton bouton-discret"
|
||||||
<div class="filtres">
|
@onclick="() => _filtresOuverts = !_filtresOuverts"
|
||||||
<div class="segments" role="group" aria-label="Filtrer la bibliographie">
|
aria-expanded="@_filtresOuverts">
|
||||||
<button type="button" class="segment @(_seulementManquants ? null : "segment-actif")"
|
Filtrer (@NombreFiltres)
|
||||||
@onclick="() => _seulementManquants = false">Tout</button>
|
</button>
|
||||||
<button type="button" class="segment @(_seulementManquants ? "segment-actif" : null)"
|
|
||||||
@onclick="() => _seulementManquants = true">Ce qui me manque</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
<ul class="liste-bibliographie">
|
||||||
@foreach (var oeuvre in Affichees(biblio))
|
@foreach (var oeuvre in Affichees(biblio))
|
||||||
@@ -204,16 +229,51 @@ else if (_bibliographie is { } biblio)
|
|||||||
|
|
||||||
@if (oeuvre.ADecouvrir)
|
@if (oeuvre.ADecouvrir)
|
||||||
{
|
{
|
||||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
<div class="actions-formulaire">
|
||||||
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
|
<label class="selection-oeuvre">
|
||||||
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
|
<input type="checkbox"
|
||||||
Je le veux
|
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>
|
</button>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</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
|
@* 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
|
franchement vaut mieux que laisser l'utilisateur découvrir seul qu'un livre qu'il
|
||||||
possède n'est pas grisé. *@
|
possède n'est pas grisé. *@
|
||||||
@@ -239,7 +299,9 @@ else if (_bibliographie is { } biblio)
|
|||||||
public int AuteurId { get; set; }
|
public int AuteurId { get; set; }
|
||||||
|
|
||||||
private BibliographieDto? _bibliographie;
|
private BibliographieDto? _bibliographie;
|
||||||
private bool _seulementManquants;
|
private int _modePossession;
|
||||||
|
private bool _filtresOuverts;
|
||||||
|
private bool _afficherMasquees;
|
||||||
private bool _enCours;
|
private bool _enCours;
|
||||||
private bool _chargement;
|
private bool _chargement;
|
||||||
|
|
||||||
@@ -250,13 +312,22 @@ else if (_bibliographie is { } biblio)
|
|||||||
private string? _message;
|
private string? _message;
|
||||||
|
|
||||||
private int _possedees;
|
private int _possedees;
|
||||||
|
private int _masquees;
|
||||||
private IReadOnlyList<LivreDto> _livresCatalogue = [];
|
private IReadOnlyList<LivreDto> _livresCatalogue = [];
|
||||||
private readonly HashSet<string> _oeuvresOuvertes = [];
|
private readonly HashSet<string> _oeuvresOuvertes = [];
|
||||||
|
private readonly HashSet<string> _selection = [];
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync() => await ChargerAsync();
|
protected override async Task OnParametersSetAsync() => await ChargerAsync();
|
||||||
|
|
||||||
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
|
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()
|
private async Task ChargerAsync()
|
||||||
{
|
{
|
||||||
@@ -278,6 +349,7 @@ else if (_bibliographie is { } biblio)
|
|||||||
}
|
}
|
||||||
|
|
||||||
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
|
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
|
||||||
|
_masquees = _bibliographie.Oeuvres.Count(o => o.Masquee);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
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)
|
private void BasculerOeuvre(string titre)
|
||||||
{
|
{
|
||||||
if (!_oeuvresOuvertes.Add(titre))
|
if (!_oeuvresOuvertes.Add(titre))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace MaBibli.Client.Services;
|
|||||||
public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, EtatReseau reseau)
|
public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, EtatReseau reseau)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||||
|
private readonly Dictionary<int, BibliographieDto> _bibliographies = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recharge tous les instantanés depuis le réseau.
|
/// 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(
|
public async Task<ResultatApi<SouhaitDto>> AjouterSouhaitAsync(
|
||||||
EnregistrementSouhait saisie, CancellationToken ct = default) =>
|
EnregistrementSouhait saisie, CancellationToken ct = default)
|
||||||
await EcrireAsync<SouhaitDto>(() => http.PostAsJsonAsync("api/souhaits", saisie, Json, ct), ct);
|
{
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Retire une envie. <c>null</c> si c'est fait, sinon le motif à afficher.
|
/// 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(
|
public async Task<BibliographieDto?> ObtenirBibliographieAsync(
|
||||||
int auteurId, CancellationToken ct = default)
|
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);
|
var reponse = await http.GetAsync($"api/auteurs/{auteurId}/bibliographie", ct);
|
||||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
var bibliographie = reponse.StatusCode == HttpStatusCode.NotFound
|
||||||
? null
|
? null
|
||||||
: await LireAsync<BibliographieDto>(reponse, ct);
|
: 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; }
|
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>
|
/// <summary>Formats d'export de la liste d'envies.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Les deux usages décrits dans IDEES.md ne demandent pas la même chose : « l'emporter en
|
/// 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; }
|
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>
|
/// <summary>Ni possédé, ni déjà souhaité : ce qui reste à découvrir.</summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool ADecouvrir => !Possede && !Souhaite;
|
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