Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur
La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie frontière : toute lecture filtre dessus. Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux filtres mesurés sur des réponses réelles sont indispensables — le type de document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs : « Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola. Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51 œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui désigne une édition et non une œuvre ; ses limites sont dites à l'écran. Export en deux formats, tous deux du texte sans dépendance : .txt groupé par auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le tableur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,13 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
|
||||
public DbSet<Pret> Prets => Set<Pret>();
|
||||
|
||||
/// <summary>
|
||||
/// Liste d'envies, <b>personnelle</b>. Table séparée des <see cref="Livres"/> : un livre
|
||||
/// souhaité n'est pas un livre possédé, et ne doit apparaître ni au catalogue, ni dans les
|
||||
/// compteurs, ni dans les prêts.
|
||||
/// </summary>
|
||||
public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@@ -90,6 +97,30 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
|
||||
refus.HasIndex(r => new { r.AuteurAId, r.AuteurBId }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<LivreSouhaite>(souhait =>
|
||||
{
|
||||
souhait.Property(s => s.Utilisateur).IsRequired();
|
||||
souhait.Property(s => s.Titre).IsRequired();
|
||||
souhait.Property(s => s.TitreNormalise).IsRequired();
|
||||
|
||||
// Jamais nullable : SQLite tient deux NULL pour distincts, et l'unicité ci-dessous
|
||||
// laisserait alors passer autant de doublons qu'on veut sur les envies sans auteur.
|
||||
souhait.Property(s => s.AuteurNormalise).IsRequired().HasDefaultValue(string.Empty);
|
||||
|
||||
// Toute lecture part de là : la liste est personnelle, on la parcourt par personne.
|
||||
souhait.HasIndex(s => s.Utilisateur, "IX_LivresSouhaites_Utilisateur");
|
||||
|
||||
// Une même œuvre ne s'ajoute qu'une fois par personne — l'écran de bibliographie
|
||||
// rend le double clic facile. L'auteur fait partie de la clé : deux auteurs peuvent
|
||||
// avoir publié deux « Œuvres » différentes, et l'un ne doit pas bloquer l'autre.
|
||||
// Deux personnes peuvent en revanche souhaiter le même livre, d'où l'utilisateur
|
||||
// en tête de l'index.
|
||||
souhait.HasIndex(
|
||||
s => new { s.Utilisateur, s.TitreNormalise, s.AuteurNormalise },
|
||||
"IX_LivresSouhaites_Utilisateur_Oeuvre")
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Pret>(pret =>
|
||||
{
|
||||
pret.Property(p => p.Emprunteur).IsRequired();
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// <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("20260818052834_ListeDEnvies")]
|
||||
partial class ListeDEnvies
|
||||
{
|
||||
/// <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.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.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.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<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.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.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.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.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.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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MaBibli.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ListeDEnvies : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LivresSouhaites",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Utilisateur = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Titre = table.Column<string>(type: "TEXT", nullable: false),
|
||||
TitreNormalise = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Auteur = table.Column<string>(type: "TEXT", nullable: true),
|
||||
AuteurNormalise = table.Column<string>(type: "TEXT", nullable: false, defaultValue: ""),
|
||||
Editeur = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Annee = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Isbn = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CoverUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Note = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateAjout = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LivresSouhaites", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LivresSouhaites_Utilisateur",
|
||||
table: "LivresSouhaites",
|
||||
column: "Utilisateur");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LivresSouhaites_Utilisateur_Oeuvre",
|
||||
table: "LivresSouhaites",
|
||||
columns: new[] { "Utilisateur", "TitreNormalise", "AuteurNormalise" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LivresSouhaites");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,61 @@ namespace MaBibli.Api.Data.Migrations
|
||||
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<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.Pret", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Text;
|
||||
using MaBibli.Api.Services.Catalogue;
|
||||
using MaBibli.Api.Services.Identite;
|
||||
using MaBibli.Api.Services.Souhaits;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Api.Endpoints;
|
||||
|
||||
public static class SouhaitsEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// La liste d'envies et son export.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Aucun point d'entrée n'accepte de nom d'utilisateur</b>, et il ne doit jamais y en
|
||||
/// avoir : la liste rendue est toujours celle de l'appelant, identifié par les en-têtes
|
||||
/// SSOwat. C'est la même règle que pour les statuts de lecture, et elle est ici encore plus
|
||||
/// nette — le sens de la fonctionnalité est de préparer un cadeau sans que l'autre le voie.
|
||||
/// </remarks>
|
||||
public static IEndpointRouteBuilder MapSouhaitsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var groupe = routes.MapGroup("/api/souhaits").WithTags("Liste d'envies");
|
||||
|
||||
groupe.MapGet("/", async (
|
||||
IServiceSouhaits service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
Results.Ok(await service.ListerAsync(utilisateurs.Obtenir().Identifiant, ct)))
|
||||
.WithName("ListerSouhaits")
|
||||
.WithSummary("La liste d'envies de l'appelant, groupée par auteur.")
|
||||
.WithDescription(
|
||||
"Personnelle : deux membres du foyer obtiennent deux listes différentes. "
|
||||
+ "Sans identité, la liste est vide.")
|
||||
.Produces<IReadOnlyList<SouhaitDto>>();
|
||||
|
||||
groupe.MapPost("/", async (
|
||||
EnregistrementSouhait saisie,
|
||||
IServiceSouhaits service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var resultat = await service.AjouterAsync(
|
||||
saisie, utilisateurs.Obtenir().Identifiant, ct);
|
||||
|
||||
return resultat.Erreur is not null
|
||||
? Results.BadRequest(new { erreur = resultat.Erreur })
|
||||
: Results.Created($"/api/souhaits/{resultat.Souhait!.Id}", resultat.Souhait);
|
||||
})
|
||||
.WithName("AjouterSouhait")
|
||||
.WithSummary("Ajoute un livre à la liste d'envies de l'appelant.")
|
||||
.Produces<SouhaitDto>(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
groupe.MapDelete("/{id:int}", async (
|
||||
int id,
|
||||
IServiceSouhaits service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var supprime = await service.SupprimerAsync(
|
||||
id, utilisateurs.Obtenir().Identifiant, ct);
|
||||
|
||||
return supprime ? Results.NoContent() : Results.NotFound();
|
||||
})
|
||||
.WithName("SupprimerSouhait")
|
||||
.WithSummary("Retire un livre de la liste d'envies de l'appelant.")
|
||||
.WithDescription(
|
||||
"404 si l'envie n'existe pas OU appartient à quelqu'un d'autre : les deux cas "
|
||||
+ "sont volontairement indiscernables.")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// Deux formats parce que les deux usages décrits dans IDEES.md diffèrent : le .txt
|
||||
// s'emporte en librairie et se lit tel quel, le .csv s'ouvre dans un tableur pour se
|
||||
// répartir des achats. Ce sont deux fichiers texte : aucune dépendance ni pour les
|
||||
// produire, ni pour les lire.
|
||||
groupe.MapGet("/export.txt", (
|
||||
IServiceSouhaits service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) =>
|
||||
ExporterAsync(FormatExportSouhaits.Texte, service, utilisateurs, ct))
|
||||
.WithName("ExporterSouhaitsTexte")
|
||||
.WithSummary("Liste d'envies en texte lisible, groupée par auteur.")
|
||||
.ExcludeFromDescription();
|
||||
|
||||
groupe.MapGet("/export.csv", (
|
||||
IServiceSouhaits service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) =>
|
||||
ExporterAsync(FormatExportSouhaits.Csv, service, utilisateurs, ct))
|
||||
.WithName("ExporterSouhaitsCsv")
|
||||
.WithSummary("Liste d'envies en CSV, ouvrable dans un tableur.")
|
||||
.ExcludeFromDescription();
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produit le fichier d'export et le renvoie en <b>téléchargement</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le nom de fichier passé à <c>Results.File</c> pose un <c>Content-Disposition:
|
||||
/// attachment</c> : le navigateur enregistre au lieu d'afficher, y compris sur mobile où un
|
||||
/// .txt s'ouvrirait sinon dans l'onglet. C'est ce qui rend l'export réellement
|
||||
/// « emportable ».
|
||||
/// <para>
|
||||
/// Le fichier est produit ici plutôt que côté client : le client Blazor n'a pas la liste
|
||||
/// complète sous la main, et un export généré par le serveur reste correct même si l'écran
|
||||
/// affiche une vue filtrée.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static async Task<IResult> ExporterAsync(
|
||||
FormatExportSouhaits format,
|
||||
IServiceSouhaits service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var utilisateur = utilisateurs.Obtenir();
|
||||
var souhaits = await service.ListerAsync(utilisateur.Identifiant, ct);
|
||||
var maintenant = DateTime.Now;
|
||||
|
||||
var contenu = ExportSouhaits.Produire(
|
||||
format, souhaits, utilisateur.Affichage, maintenant);
|
||||
|
||||
// Le BOM n'est mis QUE sur le CSV, où il est indispensable : sans lui, Excel lit le
|
||||
// fichier en codage hérité et affiche « Émile Zola ». Le .txt s'en passe — il est fait
|
||||
// pour être lu tel quel, et certains lecteurs simples affichent le BOM comme un
|
||||
// caractère parasite en tête de la première ligne.
|
||||
var corps = Encoding.UTF8.GetBytes(contenu);
|
||||
var octets = format == FormatExportSouhaits.Csv
|
||||
? [.. ExportSouhaits.Bom, .. corps]
|
||||
: corps;
|
||||
|
||||
return Results.File(
|
||||
octets,
|
||||
ExportSouhaits.TypeMime(format),
|
||||
ExportSouhaits.NomFichier(format, maintenant));
|
||||
}
|
||||
|
||||
/// <summary>Bibliographie d'un auteur du catalogue, d'après la BnF.</summary>
|
||||
public static IEndpointRouteBuilder MapBibliographieEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
routes.MapGet("/api/auteurs/{id:int}/bibliographie", async (
|
||||
int id,
|
||||
IServiceBibliographie service,
|
||||
IFournisseurUtilisateur utilisateurs,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var bibliographie = await service.ObtenirAsync(
|
||||
id, utilisateurs.Obtenir().Identifiant, ct);
|
||||
|
||||
return bibliographie is null ? Results.NotFound() : Results.Ok(bibliographie);
|
||||
})
|
||||
.WithName("BibliographieAuteur")
|
||||
.WithTags("Auteurs")
|
||||
.WithSummary("Ce que la BnF connaît de cet auteur, confronté à l'étagère.")
|
||||
.WithDescription(
|
||||
"« Possédé » vaut pour tout le foyer, « souhaité » seulement pour l'appelant. "
|
||||
+ "Le rapprochement se fait par titre normalisé : il peut manquer un livre "
|
||||
+ "possédé sous un autre titre, d'où le fait qu'aucune ligne ne soit masquée.")
|
||||
.Produces<BibliographieDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using MaBibli.Api.Services.Catalogue;
|
||||
using MaBibli.Api.Services.Identite;
|
||||
using MaBibli.Api.Services.Isbn;
|
||||
using MaBibli.Api.Services.Prets;
|
||||
using MaBibli.Api.Services.Souhaits;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -16,6 +17,8 @@ builder.Services.AddDbContext<MaBibliDbContext>(options =>
|
||||
builder.Services.AddScoped<IServiceAuteurs, ServiceAuteurs>();
|
||||
builder.Services.AddScoped<IServiceCatalogue, ServiceCatalogue>();
|
||||
builder.Services.AddScoped<IServicePrets, ServicePrets>();
|
||||
builder.Services.AddScoped<IServiceSouhaits, ServiceSouhaits>();
|
||||
builder.Services.AddScoped<IServiceBibliographie, ServiceBibliographie>();
|
||||
|
||||
// Identité : lue dans les en-têtes injectés par SSOwat, avec repli sur un utilisateur simulé
|
||||
// configuré en développement (il n'y a pas de portail YunoHost en local).
|
||||
@@ -73,6 +76,8 @@ app.MapIsbnEndpoints();
|
||||
app.MapLivresEndpoints();
|
||||
app.MapAuteursEndpoints();
|
||||
app.MapPretsEndpoints();
|
||||
app.MapSouhaitsEndpoints();
|
||||
app.MapBibliographieEndpoints();
|
||||
app.MapIdentiteEndpoints();
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
using MaBibli.Api.Data;
|
||||
using MaBibli.Api.Services.Isbn;
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Textes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MaBibli.Api.Services.Catalogue;
|
||||
|
||||
public interface IServiceBibliographie
|
||||
{
|
||||
/// <summary>
|
||||
/// Bibliographie d'un auteur du catalogue, confrontée à ce que l'on possède et à ce que
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bibliographie d'un auteur : ce que la BnF connaît de lui, mis en regard de l'étagère.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trois sources se rencontrent ici, et elles n'ont pas la même portée :
|
||||
/// <list type="bullet">
|
||||
/// <item>la <b>BnF</b>, source externe, ni possédée ni personnelle ;</item>
|
||||
/// <item>le <b>catalogue</b>, commun au foyer — « possédé » veut dire possédé par tout le
|
||||
/// monde ;</item>
|
||||
/// <item>la <b>liste d'envies</b>, personnelle — « déjà souhaité » ne vaut que pour l'appelant.</item>
|
||||
/// </list>
|
||||
/// Deux membres du foyer voient donc la même colonne « possédé » et deux colonnes « souhaité »
|
||||
/// différentes. C'est voulu, et c'est ce qui permet d'offrir un livre à l'autre.
|
||||
/// </remarks>
|
||||
public sealed class ServiceBibliographie(
|
||||
MaBibliDbContext db, IBnfClient bnf, IServiceAuteurs auteurs) : IServiceBibliographie
|
||||
{
|
||||
public async Task<BibliographieDto?> ObtenirAsync(
|
||||
int auteurId, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
var auteur = await auteurs.ObtenirAsync(auteurId, ct);
|
||||
if (auteur is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var (resultat, avertissement) = await bnf.RechercherParAuteurAsync(auteur.Nom, ct);
|
||||
|
||||
// Ce que l'on possède DE CET AUTEUR. On ne compare pas à tout le catalogue : deux auteurs
|
||||
// peuvent avoir écrit deux livres homonymes, et griser l'un à cause de l'autre serait un
|
||||
// faux positif — bien plus gênant qu'un oubli, puisqu'il détourne d'un achat justifié.
|
||||
var possedes = await db.LivreAuteurs
|
||||
.AsNoTracking()
|
||||
.Where(la => la.AuteurId == auteurId)
|
||||
.Select(la => new { la.LivreId, la.Livre!.Titre, la.Livre.Isbn })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var parTitre = new Dictionary<string, int>();
|
||||
var parIsbn = new Dictionary<string, int>();
|
||||
|
||||
foreach (var livre in possedes)
|
||||
{
|
||||
var cle = CleOeuvre.Cle(livre.Titre);
|
||||
if (cle.Length > 0)
|
||||
{
|
||||
parTitre.TryAdd(cle, livre.LivreId);
|
||||
}
|
||||
|
||||
// L'ISBN ne sert qu'en confirmation : il ne rapproche que la même édition exacte,
|
||||
// alors que le titre rapproche l'œuvre. Il rattrape en revanche le cas où le titre
|
||||
// saisi à la main diverge de celui de la BnF.
|
||||
if (!string.IsNullOrWhiteSpace(livre.Isbn))
|
||||
{
|
||||
parIsbn.TryAdd(livre.Isbn, livre.LivreId);
|
||||
}
|
||||
}
|
||||
|
||||
var souhaits = await SouhaitsAsync(utilisateur, ct);
|
||||
|
||||
var oeuvres = Regrouper(resultat)
|
||||
.Select(o => Confronter(o, parTitre, parIsbn, souhaits))
|
||||
.OrderByDescending(o => AnneeTriable(o.Annee))
|
||||
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return new BibliographieDto
|
||||
{
|
||||
Auteur = auteur,
|
||||
Oeuvres = oeuvres,
|
||||
NoticesAnnoncees = resultat.NombreAnnonce,
|
||||
NoticesLues = resultat.NombreLu,
|
||||
NoticesEcartees = resultat.EcarteesAutreAuteur + resultat.EcarteesTypeNonLivre,
|
||||
Avertissement = avertissement,
|
||||
};
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var lignes = await db.LivresSouhaites
|
||||
.AsNoTracking()
|
||||
.Where(s => s.Utilisateur == utilisateur)
|
||||
.Select(s => new { s.Id, s.TitreNormalise })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var index = new Dictionary<string, int>();
|
||||
foreach (var ligne in lignes)
|
||||
{
|
||||
index.TryAdd(ligne.TitreNormalise, ligne.Id);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Réunit les rééditions sous une seule œuvre.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// C'est le cœur de l'utilisabilité de l'écran : sans regroupement, Bernard Werber occupe
|
||||
/// 183 lignes dont « Les fourmis » onze fois. Après regroupement, 51 lignes.
|
||||
/// <para>
|
||||
/// Le représentant retenu est la notice la <b>plus ancienne</b> : c'est l'édition originale,
|
||||
/// donc l'année qui situe l'œuvre dans la carrière de l'auteur, et non celle du dernier
|
||||
/// retirage en poche. Le titre et l'éditeur suivent la même notice pour rester cohérents,
|
||||
/// mais l'ISBN est pris sur <b>n'importe quelle</b> notice qui en porte un : les éditions
|
||||
/// anciennes n'en ont souvent aucun, et un ISBN de réédition reste utile pour commander.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEnumerable<OeuvreBibliographie> Regrouper(ResultatBibliographie resultat) =>
|
||||
resultat.Notices
|
||||
.Where(n => n.CleTitre.Length > 0)
|
||||
.GroupBy(n => n.CleTitre)
|
||||
.Select(groupe =>
|
||||
{
|
||||
var ordonnees = groupe
|
||||
.OrderBy(n => AnneeTriable(n.Annee) ?? int.MaxValue)
|
||||
.ToList();
|
||||
|
||||
var reference = ordonnees[0];
|
||||
|
||||
return new OeuvreBibliographie
|
||||
{
|
||||
Titre = reference.Titre,
|
||||
Annee = reference.Annee,
|
||||
Editeur = reference.Editeur,
|
||||
Isbn = ordonnees.Select(n => n.Isbn).FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)),
|
||||
NombreEditions = groupe.Count(),
|
||||
};
|
||||
});
|
||||
|
||||
private static OeuvreBibliographie Confronter(
|
||||
OeuvreBibliographie oeuvre,
|
||||
IReadOnlyDictionary<string, int> parTitre,
|
||||
IReadOnlyDictionary<string, int> parIsbn,
|
||||
IReadOnlyDictionary<string, int> souhaits)
|
||||
{
|
||||
var cle = CleOeuvre.Cle(oeuvre.Titre);
|
||||
|
||||
int? livreId = parTitre.TryGetValue(cle, out var idTitre) ? idTitre : null;
|
||||
|
||||
if (livreId is null
|
||||
&& !string.IsNullOrWhiteSpace(oeuvre.Isbn)
|
||||
&& parIsbn.TryGetValue(oeuvre.Isbn, out var idIsbn))
|
||||
{
|
||||
livreId = idIsbn;
|
||||
}
|
||||
|
||||
int? souhaitId = souhaits.TryGetValue(cle, out var idSouhait) ? idSouhait : null;
|
||||
|
||||
return oeuvre with
|
||||
{
|
||||
Possede = livreId is not null,
|
||||
LivreId = livreId,
|
||||
Souhaite = souhaitId is not null,
|
||||
SouhaitId = souhaitId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Année exploitable pour le tri, extraite d'une date BnF.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>dc:date</c> n'est pas un nombre : on rencontre « 2012 », mais aussi « [DL 2012] »,
|
||||
/// « cop. 1991 » ou « 19..? ». On prend la première suite de quatre chiffres, et
|
||||
/// <c>null</c> s'il n'y en a pas — une date illisible ne doit pas devenir l'an zéro et
|
||||
/// remonter en tête de liste.
|
||||
/// </remarks>
|
||||
public static int? AnneeTriable(string? date)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(date))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i + 4 <= date.Length; i++)
|
||||
{
|
||||
var tranche = date.AsSpan(i, 4);
|
||||
if (int.TryParse(tranche, out var annee) && annee is >= 1000 and <= 2999)
|
||||
{
|
||||
return annee;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Xml.Linq;
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Api.Services.Isbn;
|
||||
|
||||
/// <summary>Une notice BnF réduite à ce qui sert à bâtir une bibliographie.</summary>
|
||||
public sealed record NoticeAuteur
|
||||
{
|
||||
public required string Titre { get; init; }
|
||||
|
||||
/// <summary>Clé d'œuvre du titre : c'est elle qui réunit les rééditions.</summary>
|
||||
public required string CleTitre { get; init; }
|
||||
|
||||
public string? Annee { get; init; }
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
public string? Isbn { get; init; }
|
||||
|
||||
/// <summary>Auteurs nettoyés de la notice, tous rôles confondus.</summary>
|
||||
public IReadOnlyList<string> Auteurs { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Résultat de la lecture d'une réponse SRU « par auteur ».</summary>
|
||||
public sealed record ResultatBibliographie
|
||||
{
|
||||
public IReadOnlyList<NoticeAuteur> Notices { get; init; } = [];
|
||||
|
||||
/// <summary>Nombre de notices annoncé par la BnF, tous types confondus.</summary>
|
||||
public int NombreAnnonce { get; init; }
|
||||
|
||||
/// <summary>Notices lues dans cette réponse, avant tout filtrage.</summary>
|
||||
public int NombreLu { get; init; }
|
||||
|
||||
/// <summary>Notices écartées parce qu'elles ne décrivent pas un livre (disque, jeu, film…).</summary>
|
||||
public int EcarteesTypeNonLivre { get; init; }
|
||||
|
||||
/// <summary>Notices écartées parce qu'aucun de leurs auteurs n'est celui demandé.</summary>
|
||||
public int EcarteesAutreAuteur { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lecture d'une réponse SRU de la BnF interrogée <b>par auteur</b>
|
||||
/// (<c>bib.author all "…"</c>, <c>recordSchema=dublincore</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Fonction pure, testée sur des réponses réelles enregistrées dans <c>Fixtures/</c>.
|
||||
/// <para>
|
||||
/// <b>Deux filtres, mesurés sur des réponses réelles le 2026-08-18, sont indispensables.</b>
|
||||
/// Sans eux l'écran de bibliographie ment à l'utilisateur.
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <b>Le type de document.</b> L'index auteur ne distingue pas un livre d'un livre audio, d'un
|
||||
/// jeu vidéo ou d'un spectacle. Sur 200 notices de Bernard Werber : 180 imprimés, mais aussi
|
||||
/// 13 enregistrements sonores, 3 images animées, 2 jeux multimédias et 1 spectacle. Une liste
|
||||
/// d'envies de livres n'a pas à proposer d'acheter un jeu.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <b>L'auteur réel de la notice.</b> C'est le filtre <b>critique</b>, et il n'est pas
|
||||
/// théorique. <c>bib.author all "Émile Zola"</c> rapproche les mots « émile » et « zola » sur
|
||||
/// l'<i>ensemble</i> des auteurs d'une notice : un livre signé d'un « Émile » quelconque et d'un
|
||||
/// « Zola » quelconque remonte donc aussi. Mesuré sur 200 notices d'« Émile Zola » : <b>67 sont
|
||||
/// écartées</b>, dont toute l'œuvre de sa fille Denise Le Blond-Zola (« Émile Zola raconté par
|
||||
/// sa fille », « Zola et Cézanne »), qui parle de lui sans qu'il l'ait écrite. Les proposer à
|
||||
/// l'achat comme du Zola serait faux.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Ce second filtre a un <b>coût assumé</b> : il écarte aussi les adaptations dont l'auteur n'est
|
||||
/// pas le scénariste — les bandes dessinées tirées de Werber sont signées Corbeyran, et
|
||||
/// disparaissent (17 notices sur 200). C'est le bon compromis : afficher l'œuvre d'un tiers sous
|
||||
/// le nom de l'auteur est une erreur visible et gênante, en omettre une adaptation ne l'est pas.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class BnfBibliographieParser
|
||||
{
|
||||
private static readonly XNamespace Srw = "http://www.loc.gov/zing/srw/";
|
||||
private static readonly XNamespace Dc = "http://purl.org/dc/elements/1.1/";
|
||||
private static readonly XNamespace Xml = "http://www.w3.org/XML/1998/namespace";
|
||||
|
||||
/// <summary>
|
||||
/// Types <c>dc:type</c> (en français) considérés comme des livres.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Liste <b>positive</b> et non liste d'exclusion : les types inconnus sont écartés. Une
|
||||
/// bibliographie tronquée vaut mieux qu'une bibliographie qui propose un DVD.
|
||||
/// </remarks>
|
||||
private static readonly HashSet<string> TypesLivre =
|
||||
[
|
||||
NormalisationTexte.Normaliser("texte imprimé"),
|
||||
NormalisationTexte.Normaliser("ressource électronique"),
|
||||
];
|
||||
|
||||
/// <summary>Préfixe des <c>dc:identifier</c> qui portent un ISBN : « ISBN 2226086366 ».</summary>
|
||||
private const string PrefixeIsbn = "ISBN ";
|
||||
|
||||
/// <summary>
|
||||
/// Convertit une réponse SRU en notices exploitables.
|
||||
/// </summary>
|
||||
/// <param name="xml">Corps de la réponse SRU.</param>
|
||||
/// <param name="auteurDemande">
|
||||
/// Nom de l'auteur dont on veut la bibliographie. Chaque notice doit compter cet auteur
|
||||
/// parmi les siens, au sens de <see cref="RapprochementAuteurs"/> — la même règle que celle
|
||||
/// qui regroupe les auteurs du catalogue, donc tolérante à la casse, aux accents, à l'ordre
|
||||
/// « Nom, Prénom » et aux initiales.
|
||||
/// </param>
|
||||
public static ResultatBibliographie Parser(string xml, string auteurDemande)
|
||||
{
|
||||
var doc = XDocument.Parse(xml);
|
||||
|
||||
var annonce = doc.Descendants(Srw + "numberOfRecords").FirstOrDefault()?.Value;
|
||||
var notices = new List<NoticeAuteur>();
|
||||
int lues = 0, horsType = 0, autreAuteur = 0;
|
||||
|
||||
foreach (var record in doc.Descendants(Srw + "record"))
|
||||
{
|
||||
var data = record.Element(Srw + "recordData");
|
||||
if (data is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lues++;
|
||||
|
||||
if (!EstUnLivre(data))
|
||||
{
|
||||
horsType++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var auteurs = Tous(data, "creator")
|
||||
.Select(NettoyageIsbd.Auteur)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Select(a => a!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (!auteurs.Any(a => RapprochementAuteurs.SontLeMemeAuteur(a, auteurDemande)))
|
||||
{
|
||||
autreAuteur++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var titre = NettoyageIsbd.Titre(Premier(data, "title"));
|
||||
titre = NettoyageIsbd.RetirerAuteurEnFin(titre, auteurs);
|
||||
|
||||
if (titre is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
notices.Add(new NoticeAuteur
|
||||
{
|
||||
Titre = titre,
|
||||
CleTitre = CleOeuvre.Cle(titre),
|
||||
Annee = Premier(data, "date"),
|
||||
Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
|
||||
Isbn = Isbn(data),
|
||||
Auteurs = auteurs,
|
||||
});
|
||||
}
|
||||
|
||||
return new ResultatBibliographie
|
||||
{
|
||||
Notices = notices,
|
||||
NombreAnnonce = int.TryParse(annonce, out var n) ? n : lues,
|
||||
NombreLu = lues,
|
||||
EcarteesTypeNonLivre = horsType,
|
||||
EcarteesAutreAuteur = autreAuteur,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La notice décrit-elle un livre ?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Seuls les <c>dc:type</c> en français sont consultés : les variantes anglaises sont plus
|
||||
/// grossières (« text » couvre aussi bien un imprimé qu'un enregistrement transcrit).
|
||||
/// </remarks>
|
||||
private static bool EstUnLivre(XElement data) =>
|
||||
data.Descendants(Dc + "type")
|
||||
.Where(t => (string?)t.Attribute(Xml + "lang") == "fre")
|
||||
.Select(t => NormalisationTexte.Normaliser(t.Value))
|
||||
.Any(TypesLivre.Contains);
|
||||
|
||||
/// <summary>
|
||||
/// ISBN de la notice, extrait des <c>dc:identifier</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le Dublin Core BnF mélange dans ce champ l'ark du catalogue et, quand il existe, l'ISBN
|
||||
/// préfixé du mot « ISBN ». Mesuré : 84 notices sur 100 en portent un, presque toujours sous
|
||||
/// sa forme à 10 chiffres — la BnF indexant l'ISBN tel qu'imprimé (voir CLAUDE.md).
|
||||
/// </remarks>
|
||||
private static string? Isbn(XElement data) =>
|
||||
Tous(data, "identifier")
|
||||
.Where(v => v.StartsWith(PrefixeIsbn, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(v => v[PrefixeIsbn.Length..].Trim())
|
||||
.FirstOrDefault(v => v.Length > 0);
|
||||
|
||||
private static string? Premier(XElement data, string nomLocal) =>
|
||||
Tous(data, nomLocal).FirstOrDefault();
|
||||
|
||||
private static IEnumerable<string> Tous(XElement data, string nomLocal) =>
|
||||
data.Descendants(Dc + nomLocal)
|
||||
.Select(e => e.Value?.Trim())
|
||||
.Where(v => !string.IsNullOrEmpty(v))
|
||||
.Select(v => v!);
|
||||
}
|
||||
@@ -11,6 +11,16 @@ public interface IBnfClient
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
|
||||
string isbn, string? urlCouverture, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Interroge le SRU <b>par auteur</b> pour bâtir une bibliographie.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Renvoie un résultat vide et un <c>Avertissement</c> si la BnF est injoignable : comme
|
||||
/// pour le lookup ISBN, une source indisponible ne doit pas faire échouer l'écran.
|
||||
/// </remarks>
|
||||
Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
|
||||
string auteur, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,4 +69,100 @@ public sealed class BnfClient(HttpClient http, ILogger<BnfClient> logger) : IBnf
|
||||
return ([], $"Réponse BnF illisible pour {isbn}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notices lues par page, et nombre de pages : au plus <b>200 notices</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ce plafond est un arbitrage, pas une limite de l'API. Mesuré le 2026-08-18 : une requête
|
||||
/// coûte ~0,85 s, et les deux pages sont donc lancées <b>en parallèle</b>. Aller au-delà
|
||||
/// serait long pour un gain nul dans la plupart des cas — Bernard Werber tient en 203
|
||||
/// notices, Amélie Nothomb en 277 — et resterait de toute façon insuffisant pour un classique
|
||||
/// très réédité : Émile Zola en compte 2692. Comme le SRU classe par pertinence, les deux
|
||||
/// premières pages contiennent l'essentiel de l'œuvre ; le reste est de la réédition.
|
||||
/// <para>
|
||||
/// L'interface <b>doit</b> signaler que la liste est un extrait quand la BnF en annonce plus
|
||||
/// (<c>BibliographieDto.Tronquee</c>) : une bibliographie partielle présentée comme complète
|
||||
/// ferait croire à tort qu'un livre n'existe pas.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public const int NoticesParPage = 100;
|
||||
|
||||
public const int PagesMaximum = 2;
|
||||
|
||||
public async Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
|
||||
string auteur, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(auteur))
|
||||
{
|
||||
return (new ResultatBibliographie(), null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Les pages sont lancées ensemble : à ~0,85 s l'unité, les enchaîner doublerait
|
||||
// l'attente de l'écran pour rien.
|
||||
var pages = await Task.WhenAll(
|
||||
Enumerable.Range(0, PagesMaximum)
|
||||
.Select(i => LirePageAsync(auteur, (i * NoticesParPage) + 1, ct)));
|
||||
|
||||
var lues = pages.Where(p => p is not null).Select(p => p!).ToList();
|
||||
if (lues.Count == 0)
|
||||
{
|
||||
return (new ResultatBibliographie(), $"La BnF n'a pas répondu pour « {auteur} ».");
|
||||
}
|
||||
|
||||
return (Fusionner(lues), null);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||
{
|
||||
logger.LogWarning(ex, "BnF injoignable pour l'auteur {Auteur}", auteur);
|
||||
return (new ResultatBibliographie(), $"BnF injoignable ({ex.GetType().Name}) pour « {auteur} ».");
|
||||
}
|
||||
catch (System.Xml.XmlException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Réponse BnF illisible pour l'auteur {Auteur}", auteur);
|
||||
return (new ResultatBibliographie(), $"Réponse BnF illisible pour « {auteur} ».");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ResultatBibliographie?> LirePageAsync(
|
||||
string auteur, int premiereNotice, CancellationToken ct)
|
||||
{
|
||||
// bib.author : index auteur du catalogue général, vérifié le 2026-08-18.
|
||||
// « all » exige que tous les mots soient présents — mais sur l'ENSEMBLE des auteurs de la
|
||||
// notice, d'où le post-filtre de BnfBibliographieParser.
|
||||
var url = "api/SRU"
|
||||
+ "?version=1.2"
|
||||
+ "&operation=searchRetrieve"
|
||||
+ $"&query={Uri.EscapeDataString($"bib.author all \"{auteur}\"")}"
|
||||
+ "&recordSchema=dublincore"
|
||||
+ $"&maximumRecords={NoticesParPage}"
|
||||
+ $"&startRecord={premiereNotice}";
|
||||
|
||||
using var reponse = await http.GetAsync(url, ct);
|
||||
if (!reponse.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"BnF a répondu {Code} pour l'auteur {Auteur} (notice {Debut})",
|
||||
(int)reponse.StatusCode, auteur, premiereNotice);
|
||||
return null;
|
||||
}
|
||||
|
||||
var xml = await reponse.Content.ReadAsStringAsync(ct);
|
||||
return BnfBibliographieParser.Parser(xml, auteur);
|
||||
}
|
||||
|
||||
/// <summary>Recolle les pages en un seul résultat, en additionnant les compteurs.</summary>
|
||||
private static ResultatBibliographie Fusionner(IReadOnlyList<ResultatBibliographie> pages) =>
|
||||
new()
|
||||
{
|
||||
Notices = pages.SelectMany(p => p.Notices).ToList(),
|
||||
|
||||
// Le total annoncé est le même sur chaque page : on le prend, on ne l'additionne pas.
|
||||
NombreAnnonce = pages.Max(p => p.NombreAnnonce),
|
||||
NombreLu = pages.Sum(p => p.NombreLu),
|
||||
EcarteesTypeNonLivre = pages.Sum(p => p.EcarteesTypeNonLivre),
|
||||
EcarteesAutreAuteur = pages.Sum(p => p.EcarteesAutreAuteur),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Api.Services.Souhaits;
|
||||
|
||||
/// <summary>
|
||||
/// Sérialisation de la liste d'envies en fichier emportable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Fonctions pures</b> : elles prennent une liste et rendent une chaîne, sans base ni HTTP.
|
||||
/// C'est ce qui permet de vérifier le contenu exact du fichier en test plutôt que de se fier à
|
||||
/// une inspection visuelle.
|
||||
/// <para>
|
||||
/// <b>Aucune dépendance, ni côté production ni côté lecture.</b> IDEES.md demandait un format
|
||||
/// « lisible partout » : ni PDF, ni tableur binaire, ni JSON. Les deux sorties sont du texte.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class ExportSouhaits
|
||||
{
|
||||
/// <summary>
|
||||
/// Séparateur du CSV : le <b>point-virgule</b>, pas la virgule.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le CSV n'a qu'un seul travail — s'ouvrir proprement dans un tableur. Or Excel en locale
|
||||
/// française attend le point-virgule comme séparateur de liste : un fichier à virgules y
|
||||
/// atterrit intégralement dans la colonne A, ce qui rend l'export inutilisable pour l'usage
|
||||
/// visé (se répartir des achats avant un anniversaire). La collection étant francophone,
|
||||
/// c'est ce cas-là qu'il faut servir. Les titres contenant un point-virgule restent corrects :
|
||||
/// ils sont entre guillemets, conformément à RFC 4180.
|
||||
/// </remarks>
|
||||
public const char SeparateurCsv = ';';
|
||||
|
||||
/// <summary>
|
||||
/// Marque d'ordre des octets, <b>indispensable</b> en tête du CSV.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sans elle, Excel lit un CSV en codage hérité et affiche « Émile Zola ». Sur une liste de
|
||||
/// livres français, à peu près chaque ligne serait touchée. Le BOM ne gêne aucun autre
|
||||
/// lecteur (LibreOffice, tableurs en ligne, éditeurs de texte).
|
||||
/// </remarks>
|
||||
public static readonly byte[] Bom = Encoding.UTF8.GetPreamble();
|
||||
|
||||
public static string NomFichier(FormatExportSouhaits format, DateTime maintenant) =>
|
||||
$"liste-envies-{maintenant:yyyy-MM-dd}.{(format == FormatExportSouhaits.Csv ? "csv" : "txt")}";
|
||||
|
||||
public static string TypeMime(FormatExportSouhaits format) =>
|
||||
format == FormatExportSouhaits.Csv ? "text/csv; charset=utf-8" : "text/plain; charset=utf-8";
|
||||
|
||||
public static string Produire(
|
||||
FormatExportSouhaits format, IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant) =>
|
||||
format switch
|
||||
{
|
||||
FormatExportSouhaits.Csv => Csv(souhaits),
|
||||
_ => Texte(souhaits, proprietaire, maintenant),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Liste lisible telle quelle, groupée par auteur — la version qu'on emporte en librairie.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le groupement par auteur n'est pas cosmétique : c'est ainsi qu'une librairie est rangée.
|
||||
/// Une liste à plat obligerait à parcourir tout le fichier pour chaque rayon.
|
||||
/// </remarks>
|
||||
public static string Texte(IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant)
|
||||
{
|
||||
var sortie = new StringBuilder();
|
||||
|
||||
sortie.Append("Liste d'envies");
|
||||
if (!string.IsNullOrWhiteSpace(proprietaire))
|
||||
{
|
||||
sortie.Append(" de ").Append(proprietaire);
|
||||
}
|
||||
|
||||
sortie.Append(" — ").Append(maintenant.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)).Append('\n');
|
||||
sortie.Append(new string('=', 40)).Append('\n');
|
||||
|
||||
if (souhaits.Count == 0)
|
||||
{
|
||||
sortie.Append("\n(aucun livre dans la liste)\n");
|
||||
return sortie.ToString();
|
||||
}
|
||||
|
||||
sortie.Append('\n').Append(souhaits.Count).Append(souhaits.Count > 1 ? " livres" : " livre").Append('\n');
|
||||
|
||||
// « Auteur inconnu » est rejeté en fin de liste : ce sont les entrées les moins
|
||||
// exploitables en rayon, elles ne doivent pas ouvrir le fichier.
|
||||
var groupes = souhaits
|
||||
.GroupBy(s => string.IsNullOrWhiteSpace(s.Auteur) ? null : s.Auteur!.Trim())
|
||||
.OrderBy(g => g.Key is null)
|
||||
.ThenBy(g => g.Key, StringComparer.CurrentCultureIgnoreCase);
|
||||
|
||||
foreach (var groupe in groupes)
|
||||
{
|
||||
sortie.Append('\n').Append(groupe.Key ?? "Auteur non précisé").Append('\n');
|
||||
|
||||
foreach (var souhait in groupe.OrderBy(s => s.Titre, StringComparer.CurrentCultureIgnoreCase))
|
||||
{
|
||||
sortie.Append(" - ").Append(souhait.Titre);
|
||||
|
||||
// Éditeur, année et ISBN sur la même ligne, entre parenthèses : c'est ce qu'on
|
||||
// épelle à un libraire, et ça reste lisible sur un écran de téléphone.
|
||||
var precisions = new[] { souhait.Editeur, souhait.Annee, Isbn(souhait.Isbn) }
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.ToList();
|
||||
|
||||
if (precisions.Count > 0)
|
||||
{
|
||||
sortie.Append(" (").Append(string.Join(", ", precisions)).Append(')');
|
||||
}
|
||||
|
||||
sortie.Append('\n');
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(souhait.Note))
|
||||
{
|
||||
sortie.Append(" ").Append(souhait.Note.Trim()).Append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sortie.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Tableau ouvrable dans un tableur, une envie par ligne.</summary>
|
||||
public static string Csv(IReadOnlyList<SouhaitDto> souhaits)
|
||||
{
|
||||
var sortie = new StringBuilder();
|
||||
|
||||
Ligne(sortie, "Titre", "Auteur", "Éditeur", "Année", "ISBN", "Note", "Ajouté le");
|
||||
|
||||
foreach (var souhait in souhaits)
|
||||
{
|
||||
Ligne(
|
||||
sortie,
|
||||
souhait.Titre,
|
||||
souhait.Auteur,
|
||||
souhait.Editeur,
|
||||
souhait.Annee,
|
||||
souhait.Isbn,
|
||||
souhait.Note,
|
||||
souhait.DateAjout.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return sortie.ToString();
|
||||
}
|
||||
|
||||
private static string? Isbn(string? isbn) =>
|
||||
string.IsNullOrWhiteSpace(isbn) ? null : $"ISBN {isbn}";
|
||||
|
||||
private static void Ligne(StringBuilder sortie, params string?[] champs)
|
||||
{
|
||||
sortie.AppendJoin(SeparateurCsv, champs.Select(Echapper));
|
||||
|
||||
// CRLF : RFC 4180 le prescrit, et c'est ce qu'attendent les tableurs sous Windows.
|
||||
sortie.Append("\r\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Échappement RFC 4180 : guillemets doublés, champ encadré dès qu'il contient un
|
||||
/// séparateur, un guillemet ou un saut de ligne.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Une note libre peut contenir n'importe quoi : sans échappement, un simple « offert ; à
|
||||
/// rendre » décalerait toutes les colonnes suivantes.
|
||||
/// </remarks>
|
||||
private static string Echapper(string? champ)
|
||||
{
|
||||
var valeur = champ ?? string.Empty;
|
||||
|
||||
var doitEncadrer = valeur.Contains(SeparateurCsv)
|
||||
|| valeur.Contains('"')
|
||||
|| valeur.Contains('\n')
|
||||
|| valeur.Contains('\r');
|
||||
|
||||
return doitEncadrer ? $"\"{valeur.Replace("\"", "\"\"")}\"" : valeur;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using MaBibli.Api.Data;
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Entites;
|
||||
using MaBibli.Shared.Isbn;
|
||||
using MaBibli.Shared.Textes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MaBibli.Api.Services.Souhaits;
|
||||
|
||||
/// <summary>Issue d'un ajout d'envie : soit la ligne créée, soit un message pour l'utilisateur.</summary>
|
||||
public readonly record struct ResultatSouhait(SouhaitDto? Souhait, string? Erreur)
|
||||
{
|
||||
public static ResultatSouhait Ok(SouhaitDto souhait) => new(souhait, null);
|
||||
|
||||
public static ResultatSouhait Invalide(string message) => new(null, message);
|
||||
}
|
||||
|
||||
public interface IServiceSouhaits
|
||||
{
|
||||
Task<IReadOnlyList<SouhaitDto>> ListerAsync(string? utilisateur, CancellationToken ct = default);
|
||||
|
||||
Task<ResultatSouhait> AjouterAsync(
|
||||
EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default);
|
||||
|
||||
Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La liste d'envies, <b>personnelle</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ <b>Toute méthode filtre sur <paramref name="utilisateur"/>, sans exception.</b> C'est la
|
||||
/// différence de fond avec <c>ServiceCatalogue</c>, dont les lectures ne filtrent jamais sur
|
||||
/// <c>AjoutePar</c> : le catalogue est commun, la liste d'envies ne l'est pas. Une envie qui
|
||||
/// fuiterait vers un autre membre du foyer gâcherait exactement ce que la fonctionnalité sert
|
||||
/// à préparer.
|
||||
/// <para>
|
||||
/// Sans identité (ni en-tête SSOwat ni utilisateur simulé), il n'y a pas de liste : on ne rend
|
||||
/// rien et on n'écrit rien, plutôt que de rattacher des envies à un propriétaire inventé.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
{
|
||||
public async Task<IReadOnlyList<SouhaitDto>> ListerAsync(
|
||||
string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var souhaits = await db.LivresSouhaites
|
||||
.AsNoTracking()
|
||||
.Where(s => s.Utilisateur == utilisateur)
|
||||
.OrderBy(s => s.AuteurNormalise == string.Empty)
|
||||
.ThenBy(s => s.AuteurNormalise)
|
||||
.ThenBy(s => s.TitreNormalise)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return souhaits.Select(Projeter).ToList();
|
||||
}
|
||||
|
||||
public async Task<ResultatSouhait> AjouterAsync(
|
||||
EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return ResultatSouhait.Invalide(
|
||||
"Impossible d'ajouter une envie sans savoir à qui elle appartient.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(saisie.Titre))
|
||||
{
|
||||
return ResultatSouhait.Invalide("Le titre est obligatoire.");
|
||||
}
|
||||
|
||||
// L'ISBN reste facultatif — on souhaite souvent une œuvre sans avoir choisi son édition —
|
||||
// mais s'il est saisi il doit tenir debout, comme pour un livre du catalogue.
|
||||
string? isbn = null;
|
||||
if (!string.IsNullOrWhiteSpace(saisie.Isbn))
|
||||
{
|
||||
var normalise = IsbnUtils.Normaliser(saisie.Isbn);
|
||||
if (!IsbnUtils.EstValide(normalise))
|
||||
{
|
||||
return ResultatSouhait.Invalide(
|
||||
$"« {saisie.Isbn} » n'est pas un ISBN valide. Laissez le champ vide si vous ne l'avez pas.");
|
||||
}
|
||||
|
||||
isbn = normalise;
|
||||
}
|
||||
|
||||
var souhait = new LivreSouhaite
|
||||
{
|
||||
Utilisateur = utilisateur,
|
||||
Titre = saisie.Titre,
|
||||
Auteur = saisie.Auteur,
|
||||
Editeur = Vide(saisie.Editeur),
|
||||
Annee = Vide(saisie.Annee),
|
||||
Isbn = isbn,
|
||||
CoverUrl = Vide(saisie.CoverUrl),
|
||||
Note = Vide(saisie.Note),
|
||||
DateAjout = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
souhait.RecalculerFormes();
|
||||
|
||||
// Le doublon est refusé avec un message plutôt que laissé à l'index unique : l'écran de
|
||||
// bibliographie rend le double clic facile, et « UNIQUE constraint failed » ne veut rien
|
||||
// dire pour l'utilisateur.
|
||||
var deja = await db.LivresSouhaites.AnyAsync(
|
||||
s => s.Utilisateur == utilisateur
|
||||
&& s.TitreNormalise == souhait.TitreNormalise
|
||||
&& s.AuteurNormalise == souhait.AuteurNormalise,
|
||||
ct);
|
||||
|
||||
if (deja)
|
||||
{
|
||||
return ResultatSouhait.Invalide($"« {souhait.Titre} » est déjà dans votre liste d'envies.");
|
||||
}
|
||||
|
||||
db.LivresSouhaites.Add(souhait);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return ResultatSouhait.Ok(Projeter(souhait));
|
||||
}
|
||||
|
||||
public async Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default)
|
||||
{
|
||||
if (utilisateur is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Le filtre sur l'utilisateur fait partie de la CLÉ de recherche, pas d'une vérification
|
||||
// ultérieure : ainsi personne ne peut supprimer l'envie d'un autre en devinant son
|
||||
// identifiant. Une envie inexistante et une envie appartenant à un autre sont
|
||||
// indiscernables de l'extérieur, ce qui est bien le comportement voulu.
|
||||
var souhait = await db.LivresSouhaites
|
||||
.FirstOrDefaultAsync(s => s.Id == id && s.Utilisateur == utilisateur, ct);
|
||||
|
||||
if (souhait is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
db.LivresSouhaites.Remove(souhait);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? Vide(string? valeur) =>
|
||||
string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim();
|
||||
|
||||
/// <summary>
|
||||
/// Projection vers le DTO. <b>Sans le champ <c>Utilisateur</c></b> : il ne sort jamais de
|
||||
/// l'API, pour qu'aucun client ne puisse croire qu'il désigne de qui il parle.
|
||||
/// </summary>
|
||||
internal static SouhaitDto Projeter(LivreSouhaite souhait) => new()
|
||||
{
|
||||
Id = souhait.Id,
|
||||
Titre = souhait.Titre,
|
||||
Auteur = souhait.Auteur,
|
||||
Editeur = souhait.Editeur,
|
||||
Annee = souhait.Annee,
|
||||
Isbn = souhait.Isbn,
|
||||
CoverUrl = souhait.CoverUrl,
|
||||
Note = souhait.Note,
|
||||
DateAjout = souhait.DateAjout,
|
||||
};
|
||||
}
|
||||
@@ -70,6 +70,10 @@ else
|
||||
<li class="carte-auteur-ligne" @key="auteur.Id">
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
|
||||
@* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
|
||||
tout ce qu'il a ecrit et marquer ce qui manque. *@
|
||||
<a class="bouton bouton-compact bouton-discret"
|
||||
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@@ -77,6 +81,7 @@ else
|
||||
|
||||
<nav class="actions-flottantes">
|
||||
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
|
||||
<a class="bouton bouton-discret" href="souhaits">Ma liste d'envies</a>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
@page "/auteurs/{AuteurId:int}/bibliographie"
|
||||
@inject ServiceLivresApi Api
|
||||
|
||||
<PageTitle>MaBibli — bibliographie</PageTitle>
|
||||
|
||||
@if (_bibliographie is null && _erreur is null)
|
||||
{
|
||||
<h1 class="titre-page">Bibliographie</h1>
|
||||
<p class="message-discret">Interrogation de la BnF…</p>
|
||||
}
|
||||
else if (_erreur is not null)
|
||||
{
|
||||
<h1 class="titre-page">Bibliographie</h1>
|
||||
<p class="message-erreur" role="alert">@_erreur</p>
|
||||
}
|
||||
else if (_bibliographie is { } biblio)
|
||||
{
|
||||
<h1 class="titre-page">@biblio.Auteur.Nom</h1>
|
||||
|
||||
@if (biblio.Avertissement is not null)
|
||||
{
|
||||
<p class="message-avertissement" role="alert">@biblio.Avertissement</p>
|
||||
}
|
||||
|
||||
@if (biblio.Oeuvres.Count == 0)
|
||||
{
|
||||
<p class="message-discret">
|
||||
La BnF ne connaît aucun livre de cet auteur. C'est le cas attendu pour les auteurs
|
||||
étrangers : le dépôt légal français ne couvre que l'édition francophone.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Ces compteurs disent à l'utilisateur CE QU'IL REGARDE. Sur un auteur très réédité,
|
||||
la liste n'est qu'un extrait : le taire ferait croire qu'un livre absent n'existe pas. *@
|
||||
<p class="compteur">
|
||||
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
|
||||
— @_possedees déjà chez vous
|
||||
</p>
|
||||
|
||||
@* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments
|
||||
conditionnels dans une même phrase produit une ponctuation bancale dès qu'un des
|
||||
fragments manque (« exister. .34 notices ont été écartées »). *@
|
||||
<p class="message-discret">
|
||||
@if (biblio.Tronquee)
|
||||
{
|
||||
<text>
|
||||
D'après le catalogue de la BnF, à partir de @biblio.NoticesLues notices
|
||||
sur les <strong>@biblio.NoticesAnnoncees</strong> qu'elle annonce :
|
||||
<strong>cette liste est un extrait</strong>, classé par pertinence.
|
||||
Un livre absent d'ici peut très bien exister.
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>
|
||||
D'après le catalogue de la BnF, sur
|
||||
@biblio.NoticesLues notice@(biblio.NoticesLues > 1 ? "s" : "").
|
||||
</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (biblio.NoticesEcartees > 0)
|
||||
{
|
||||
<p class="message-discret">
|
||||
@biblio.NoticesEcartees notice@(biblio.NoticesEcartees > 1 ? "s ont" : " a")
|
||||
été écartée@(biblio.NoticesEcartees > 1 ? "s" : "") : autre support (livre audio,
|
||||
jeu) ou ouvrage signé de quelqu'un d'autre.
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (_message is not null)
|
||||
{
|
||||
<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>
|
||||
|
||||
<ul class="liste-bibliographie">
|
||||
@foreach (var oeuvre in Affichees(biblio))
|
||||
{
|
||||
@* La classe « oeuvre-possedee » grise la ligne : le livre est déjà sur l'étagère. *@
|
||||
<li class="carte-oeuvre @(oeuvre.Possede ? "oeuvre-possedee" : null)" @key="oeuvre.Titre">
|
||||
<div class="carte-corps">
|
||||
<p class="carte-titre">
|
||||
@if (oeuvre is { Possede: true, LivreId: { } livreId })
|
||||
{
|
||||
<a class="lien-auteur" href="@($"livres/{livreId}")">@oeuvre.Titre</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@oeuvre.Titre
|
||||
}
|
||||
</p>
|
||||
|
||||
<p class="carte-details">
|
||||
@if (!string.IsNullOrWhiteSpace(oeuvre.Annee))
|
||||
{
|
||||
<span>@oeuvre.Annee</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(oeuvre.Editeur))
|
||||
{
|
||||
<span>@oeuvre.Editeur</span>
|
||||
}
|
||||
@if (oeuvre.NombreEditions > 1)
|
||||
{
|
||||
<span class="etiquette">@oeuvre.NombreEditions éditions</span>
|
||||
}
|
||||
|
||||
@if (oeuvre.Possede)
|
||||
{
|
||||
<span class="etiquette etiquette-possede">Déjà chez vous</span>
|
||||
}
|
||||
else if (oeuvre.Souhaite)
|
||||
{
|
||||
<span class="etiquette etiquette-souhaite">Dans vos envies</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@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
|
||||
</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@* 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é. *@
|
||||
<p class="message-discret">
|
||||
Le rapprochement avec votre bibliothèque se fait <strong>par titre</strong>. Un livre
|
||||
que vous possédez sous un titre différent (traduction, intégrale, édition retitrée)
|
||||
n'apparaîtra donc pas comme possédé. Rien n'est masqué : à vous de trancher.
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
<nav class="actions-flottantes">
|
||||
<a class="bouton bouton-discret" href="auteurs">Retour aux auteurs</a>
|
||||
@if (_bibliographie is { } b)
|
||||
{
|
||||
<a class="bouton bouton-discret" href="@($"?auteur={b.Auteur.Id}")">Ses livres chez vous</a>
|
||||
}
|
||||
<a class="bouton bouton-discret" href="souhaits">Ma liste d'envies</a>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int AuteurId { get; set; }
|
||||
|
||||
private BibliographieDto? _bibliographie;
|
||||
private bool _seulementManquants;
|
||||
private bool _enCours;
|
||||
private string? _erreur;
|
||||
private string? _message;
|
||||
|
||||
private int _possedees;
|
||||
|
||||
protected override async Task OnParametersSetAsync() => await ChargerAsync();
|
||||
|
||||
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
|
||||
_seulementManquants ? biblio.Oeuvres.Where(o => o.ADecouvrir) : biblio.Oeuvres;
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
_erreur = null;
|
||||
|
||||
try
|
||||
{
|
||||
_bibliographie = await Api.ObtenirBibliographieAsync(AuteurId);
|
||||
|
||||
if (_bibliographie is null)
|
||||
{
|
||||
_erreur = "Cet auteur n'existe pas dans votre bibliothèque.";
|
||||
return;
|
||||
}
|
||||
|
||||
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_erreur = $"Impossible d'obtenir la bibliographie : {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ajoute une œuvre à la liste d'envies, en emportant ce que la BnF en sait.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// L'auteur enregistré est celui de l'écran, pas celui de la notice : c'est lui que
|
||||
/// l'utilisateur a en tête, et c'est sous ce nom qu'il retrouvera l'envie dans l'export.
|
||||
/// <para>
|
||||
/// Après l'ajout, la bibliographie est <b>relue</b> plutôt que corrigée en mémoire : c'est
|
||||
/// le serveur qui sait rapprocher l'envie de l'œuvre, et refaire ce calcul ici le
|
||||
/// dupliquerait — avec le risque qu'il diverge.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private async Task SouhaiterAsync(OeuvreBibliographie oeuvre)
|
||||
{
|
||||
if (_bibliographie is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enCours = true;
|
||||
_message = null;
|
||||
|
||||
var resultat = await Api.AjouterSouhaitAsync(new EnregistrementSouhait
|
||||
{
|
||||
Titre = oeuvre.Titre,
|
||||
Auteur = _bibliographie.Auteur.Nom,
|
||||
Editeur = oeuvre.Editeur,
|
||||
Annee = oeuvre.Annee,
|
||||
|
||||
// L'ISBN de la BnF est celui d'UNE édition relevée, souvent à 10 chiffres et parfois
|
||||
// épuisée. Il est repris tel quel : il aide à commander, sans engager le choix.
|
||||
Isbn = oeuvre.Isbn,
|
||||
});
|
||||
|
||||
_enCours = false;
|
||||
|
||||
if (!resultat.EstOk)
|
||||
{
|
||||
_message = resultat.Erreur;
|
||||
return;
|
||||
}
|
||||
|
||||
await ChargerAsync();
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,7 @@ else if (_livres is not null)
|
||||
<a class="bouton bouton-discret" href="ajout/manuel">Saisie manuelle</a>
|
||||
<a class="bouton bouton-discret" href="prets">Prêts</a>
|
||||
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
|
||||
<a class="bouton bouton-discret" href="souhaits">Envies</a>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
@page "/souhaits"
|
||||
@inject ServiceLivresApi Api
|
||||
|
||||
<PageTitle>MaBibli — liste d'envies</PageTitle>
|
||||
|
||||
<h1 class="titre-page">Liste d'envies</h1>
|
||||
|
||||
@* Le rappel n'est pas décoratif : la bibliothèque est commune, cette liste ne l'est pas.
|
||||
Sans ce mot, on pourrait croire que le foyer entier voit ce qu'on note ici. *@
|
||||
<p class="message-discret">
|
||||
Les livres que vous aimeriez acquérir. <strong>Cette liste est la vôtre</strong> :
|
||||
les autres membres du foyer ont la leur, et ne voient pas celle-ci.
|
||||
Ces livres ne sont pas dans le catalogue et ne comptent pas dans la bibliothèque.
|
||||
</p>
|
||||
|
||||
@if (_erreur is not null)
|
||||
{
|
||||
<p class="message-erreur" role="alert">@_erreur</p>
|
||||
}
|
||||
|
||||
@if (_souhaits is null)
|
||||
{
|
||||
<p class="message-discret">Chargement…</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="bloc-souhait-ajout">
|
||||
<h2 class="titre-section">Ajouter une envie</h2>
|
||||
|
||||
@* Deux façons d'alimenter la liste : l'ISBN quand on a le livre en main (en librairie,
|
||||
chez quelqu'un), la saisie libre quand on n'a qu'un titre. La bibliographie d'un
|
||||
auteur, elle, alimente la liste depuis l'écran des auteurs. *@
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="isbn-souhait">Pré-remplir depuis un ISBN</label>
|
||||
<div class="ligne-isbn">
|
||||
<input id="isbn-souhait" class="champ-saisie" type="text" inputmode="numeric"
|
||||
placeholder="978…" @bind="_isbn" @bind:event="oninput" />
|
||||
<button type="button" class="bouton bouton-discret"
|
||||
disabled="@(_recherche || string.IsNullOrWhiteSpace(_isbn))"
|
||||
@onclick="ChercherIsbnAsync">
|
||||
@(_recherche ? "Recherche…" : "Chercher")
|
||||
</button>
|
||||
</div>
|
||||
<p class="champ-aide">Facultatif : les champs ci-dessous restent modifiables.</p>
|
||||
</div>
|
||||
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="titre-souhait">Titre <span class="obligatoire">*</span></label>
|
||||
<input id="titre-souhait" class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput" />
|
||||
</div>
|
||||
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="auteur-souhait">Auteur</label>
|
||||
<input id="auteur-souhait" class="champ-saisie" type="text" @bind="_saisie.Auteur" @bind:event="oninput" />
|
||||
</div>
|
||||
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="note-souhait">Note</label>
|
||||
<input id="note-souhait" class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
|
||||
placeholder="« demandé à Noël », « en poche seulement »…" />
|
||||
</div>
|
||||
|
||||
<div class="actions-formulaire">
|
||||
<button type="button" class="bouton bouton-principal"
|
||||
disabled="@(_ajout || string.IsNullOrWhiteSpace(_saisie.Titre))"
|
||||
@onclick="AjouterAsync">
|
||||
Ajouter à ma liste
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (_souhaits.Count == 0)
|
||||
{
|
||||
<p class="message-discret">
|
||||
Votre liste est vide. Ajoutez un livre ci-dessus, ou ouvrez la bibliographie d'un
|
||||
auteur depuis <a href="auteurs">la liste des auteurs</a>.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="compteur">@_souhaits.Count livre@(_souhaits.Count > 1 ? "s" : "") souhaité@(_souhaits.Count > 1 ? "s" : "")</p>
|
||||
|
||||
@* Un seul bloc d'export, en haut de liste : c'est l'action qu'on vient chercher quand on
|
||||
part en librairie, elle ne doit pas être au bout d'un défilement. *@
|
||||
<div class="actions-export">
|
||||
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Texte)" download>
|
||||
Exporter en texte
|
||||
</a>
|
||||
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Csv)" download>
|
||||
Exporter en CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="liste-souhaits">
|
||||
@foreach (var souhait in _souhaits)
|
||||
{
|
||||
<li class="carte-souhait" @key="souhait.Id">
|
||||
<Couverture Url="@souhait.CoverUrl" Titre="@souhait.Titre"
|
||||
Classe="couverture-petite" Agrandissable="true" />
|
||||
|
||||
<div class="carte-corps">
|
||||
<p class="carte-titre">@souhait.Titre</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Auteur))
|
||||
{
|
||||
<p class="carte-auteur">@souhait.Auteur</p>
|
||||
}
|
||||
|
||||
<p class="carte-details">
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Editeur))
|
||||
{
|
||||
<span>@souhait.Editeur</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Annee))
|
||||
{
|
||||
<span>@souhait.Annee</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Isbn))
|
||||
{
|
||||
<span class="etiquette">ISBN @souhait.Isbn</span>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Note))
|
||||
{
|
||||
<p class="souhait-note">@souhait.Note</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="button" class="bouton bouton-compact bouton-danger"
|
||||
disabled="@_enCours" @onclick="() => SupprimerAsync(souhait)"
|
||||
aria-label="@($"Retirer « {souhait.Titre} » de la liste d'envies")">
|
||||
Retirer
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
<nav class="actions-flottantes">
|
||||
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
|
||||
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<SouhaitDto>? _souhaits;
|
||||
private EnregistrementSouhait _saisie = new();
|
||||
private string _isbn = string.Empty;
|
||||
private bool _ajout;
|
||||
private bool _recherche;
|
||||
private bool _enCours;
|
||||
private string? _erreur;
|
||||
|
||||
protected override Task OnInitializedAsync() => ChargerAsync();
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_souhaits = await Api.ListerSouhaitsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_erreur = $"Impossible de charger la liste d'envies : {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pré-remplit le formulaire depuis la cascade ISBN existante (BnF → OpenLibrary).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On retient le premier candidat sans demander de choisir : contrairement à l'ajout au
|
||||
/// catalogue, une envie ne fige pas d'édition — l'utilisateur achètera celle qu'il trouvera.
|
||||
/// Tous les champs restent modifiables.
|
||||
/// </remarks>
|
||||
private async Task ChercherIsbnAsync()
|
||||
{
|
||||
_recherche = true;
|
||||
_erreur = null;
|
||||
|
||||
try
|
||||
{
|
||||
var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());
|
||||
var candidat = resultat?.Candidats.FirstOrDefault();
|
||||
|
||||
if (candidat is null)
|
||||
{
|
||||
_erreur = $"Aucune notice trouvée pour « {_isbn.Trim()} ». Complétez à la main.";
|
||||
return;
|
||||
}
|
||||
|
||||
_saisie.Titre = candidat.Titre;
|
||||
_saisie.Auteur = string.Join(", ", candidat.Auteurs);
|
||||
_saisie.Editeur = candidat.Editeur;
|
||||
_saisie.Annee = candidat.Annee;
|
||||
_saisie.Isbn = _isbn.Trim();
|
||||
_saisie.CoverUrl = candidat.CoverUrl;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_erreur = $"La recherche a échoué : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_recherche = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AjouterAsync()
|
||||
{
|
||||
_ajout = true;
|
||||
_erreur = null;
|
||||
|
||||
var resultat = await Api.AjouterSouhaitAsync(_saisie);
|
||||
|
||||
_ajout = false;
|
||||
|
||||
if (!resultat.EstOk)
|
||||
{
|
||||
_erreur = resultat.Erreur;
|
||||
return;
|
||||
}
|
||||
|
||||
_saisie = new EnregistrementSouhait();
|
||||
_isbn = string.Empty;
|
||||
await ChargerAsync();
|
||||
}
|
||||
|
||||
private async Task SupprimerAsync(SouhaitDto souhait)
|
||||
{
|
||||
_enCours = true;
|
||||
_erreur = await Api.SupprimerSouhaitAsync(souhait.Id);
|
||||
_enCours = false;
|
||||
|
||||
if (_erreur is null)
|
||||
{
|
||||
await ChargerAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,63 @@ public sealed class ServiceLivresApi(HttpClient http)
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Liste d'envies et bibliographie par auteur.
|
||||
// Bloc ajouté en fin de classe pour rester bien délimité.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// La liste d'envies <b>de l'utilisateur courant</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Comme pour le statut de lecture, le client ne transmet aucune identité : le serveur sait
|
||||
/// qui appelle. Il n'existe volontairement pas de moyen de demander la liste d'un autre.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<SouhaitDto>> ListerSouhaitsAsync(CancellationToken ct = default) =>
|
||||
await http.GetFromJsonAsync<List<SouhaitDto>>("api/souhaits", Json, ct) ?? [];
|
||||
|
||||
public async Task<ResultatApi<SouhaitDto>> AjouterSouhaitAsync(
|
||||
EnregistrementSouhait saisie, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.PostAsJsonAsync("api/souhaits", saisie, Json, ct);
|
||||
return await InterpreterAsync<SouhaitDto>(reponse, ct);
|
||||
}
|
||||
|
||||
public async Task<string?> SupprimerSouhaitAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.DeleteAsync($"api/souhaits/{id}", ct);
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adresse de téléchargement d'un export.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le fichier n'est pas récupéré par ce service : un simple lien suffit, le serveur pose un
|
||||
/// <c>Content-Disposition: attachment</c> et le navigateur enregistre. Passer par
|
||||
/// <see cref="HttpClient"/> obligerait à fabriquer une URL d'objet en JavaScript pour un
|
||||
/// résultat identique.
|
||||
/// <para>
|
||||
/// ⚠️ Le lien <b>doit</b> porter l'attribut <c>download</c> : sans lui, le routeur Blazor
|
||||
/// intercepte le clic comme une navigation interne et affiche sa page « introuvable » au
|
||||
/// lieu de laisser le navigateur télécharger.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static string UrlExportSouhaits(FormatExportSouhaits format) =>
|
||||
format == FormatExportSouhaits.Csv ? "api/souhaits/export.csv" : "api/souhaits/export.txt";
|
||||
|
||||
/// <summary>
|
||||
/// Bibliographie d'un auteur d'après la BnF. <c>null</c> si l'auteur n'existe pas.
|
||||
/// </summary>
|
||||
public async Task<BibliographieDto?> ObtenirBibliographieAsync(
|
||||
int auteurId, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/auteurs/{auteurId}/bibliographie", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<BibliographieDto>(reponse, ct);
|
||||
}
|
||||
|
||||
private sealed record ErreurApi(string? Erreur);
|
||||
}
|
||||
|
||||
|
||||
@@ -738,3 +738,99 @@ body {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────────
|
||||
Liste d'envies et bibliographie par auteur.
|
||||
Mobile d'abord : une colonne, l'action à droite du texte, rien qui déborde.
|
||||
───────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.bloc-souhait-ajout {
|
||||
background: var(--mb-fond-doux);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Le champ ISBN et son bouton sur une ligne : le bouton ne doit pas se rétrécir
|
||||
au point de devenir intouchable au pouce quand le champ est large. */
|
||||
.ligne-isbn {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ligne-isbn .champ-saisie {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ligne-isbn .bouton {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-export {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.liste-souhaits,
|
||||
.liste-bibliographie {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.carte-souhait,
|
||||
.carte-oeuvre {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--mb-bord);
|
||||
}
|
||||
|
||||
/* Le corps prend la place restante et « min-width: 0 » l'autorise à rétrécir :
|
||||
sans lui, un titre long pousse le bouton hors de l'écran sur mobile. */
|
||||
.carte-souhait .carte-corps,
|
||||
.carte-oeuvre .carte-corps {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.carte-souhait .bouton,
|
||||
.carte-oeuvre .bouton {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.souhait-note {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--mb-texte-doux);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Le grisage des livres déjà possédés : c'est l'information principale de l'écran
|
||||
de bibliographie. L'opacité seule suffirait visuellement, mais elle ne dit rien
|
||||
à un lecteur d'écran — d'où l'étiquette « Déjà chez vous » qui l'accompagne
|
||||
toujours dans le balisage. */
|
||||
.oeuvre-possedee {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.oeuvre-possedee .carte-titre {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.etiquette-possede {
|
||||
background: #e3ece3;
|
||||
color: #2f5d34;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.etiquette-souhaite {
|
||||
background: #e6e6f5;
|
||||
color: #3b3b7a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MaBibli.Shared.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Une envie telle qu'exposée par l'API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Il n'y a <b>aucun champ « utilisateur »</b>, et il ne doit jamais y en avoir : l'API ne rend
|
||||
/// que les envies de l'appelant, qu'elle identifie par les en-têtes SSOwat. Exposer le
|
||||
/// propriétaire laisserait croire qu'on peut demander celles d'un autre.
|
||||
/// </remarks>
|
||||
public record SouhaitDto
|
||||
{
|
||||
public required int Id { get; init; }
|
||||
|
||||
public required string Titre { get; init; }
|
||||
|
||||
public string? Auteur { get; init; }
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
public string? Annee { get; init; }
|
||||
|
||||
public string? Isbn { get; init; }
|
||||
|
||||
public string? CoverUrl { get; init; }
|
||||
|
||||
public string? Note { get; init; }
|
||||
|
||||
public required DateTime DateAjout { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Charge utile d'ajout d'une envie.</summary>
|
||||
/// <remarks>
|
||||
/// Comme <see cref="EnregistrementLivre"/>, elle ne porte pas d'identité : le propriétaire est
|
||||
/// déterminé par le serveur.
|
||||
/// </remarks>
|
||||
public record EnregistrementSouhait
|
||||
{
|
||||
public string Titre { get; set; } = string.Empty;
|
||||
|
||||
public string? Auteur { get; set; }
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
|
||||
public string? Annee { get; set; }
|
||||
|
||||
public string? Isbn { get; set; }
|
||||
|
||||
public string? CoverUrl { get; set; }
|
||||
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// librairie » veut un texte qui se lit tel quel sur un téléphone, « la partager avant un
|
||||
/// anniversaire » finit souvent dans un tableur pour se répartir les achats. Ni l'un ni l'autre
|
||||
/// n'exige de dépendance : ce sont deux fichiers texte produits par du <c>string</c>.
|
||||
/// </remarks>
|
||||
public enum FormatExportSouhaits
|
||||
{
|
||||
/// <summary>Liste lisible telle quelle, groupée par auteur.</summary>
|
||||
Texte = 0,
|
||||
|
||||
/// <summary>Tableau ouvrable dans un tableur.</summary>
|
||||
Csv = 1,
|
||||
}
|
||||
|
||||
/// <summary>Une œuvre de la bibliographie d'un auteur, confrontée à ce que l'on possède déjà.</summary>
|
||||
public record OeuvreBibliographie
|
||||
{
|
||||
public required string Titre { get; init; }
|
||||
|
||||
public string? Annee { get; init; }
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
/// <summary>ISBN d'une des éditions relevées, quand la notice en portait un.</summary>
|
||||
public string? Isbn { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Nombre de notices BnF réunies sous cette œuvre — c'est-à-dire le nombre de rééditions
|
||||
/// relevées. Sert à repérer les œuvres majeures d'un auteur très réédité.
|
||||
/// </summary>
|
||||
public int NombreEditions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vrai si un livre du catalogue porte le même titre d'œuvre.
|
||||
/// <b>Rapprochement par titre, donc faillible</b> — voir <c>CleOeuvre</c>.
|
||||
/// </summary>
|
||||
public bool Possede { get; init; }
|
||||
|
||||
/// <summary>Le livre possédé, s'il y en a un : de quoi ouvrir sa fiche.</summary>
|
||||
public int? LivreId { get; init; }
|
||||
|
||||
/// <summary>Vrai si l'appelant a déjà cette œuvre dans sa liste d'envies.</summary>
|
||||
public bool Souhaite { get; init; }
|
||||
|
||||
public int? SouhaitId { get; init; }
|
||||
|
||||
/// <summary>Ni possédé, ni déjà souhaité : ce qui reste à découvrir.</summary>
|
||||
[JsonIgnore]
|
||||
public bool ADecouvrir => !Possede && !Souhaite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bibliographie d'un auteur du catalogue, telle que la BnF la connaît.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Les compteurs ne sont pas décoratifs : ils disent à l'utilisateur ce qu'il regarde. Sur un
|
||||
/// auteur très réédité (Émile Zola : 2692 notices), on ne montre qu'un <b>extrait classé par
|
||||
/// pertinence</b>, et le taire ferait passer une liste partielle pour une bibliographie complète.
|
||||
/// </remarks>
|
||||
public record BibliographieDto
|
||||
{
|
||||
public required AuteurDto Auteur { get; init; }
|
||||
|
||||
public IReadOnlyList<OeuvreBibliographie> Oeuvres { get; init; } = [];
|
||||
|
||||
/// <summary>Nombre total de notices que la BnF déclare pour cet auteur.</summary>
|
||||
public int NoticesAnnoncees { get; init; }
|
||||
|
||||
/// <summary>Nombre de notices réellement lues (plafonné).</summary>
|
||||
public int NoticesLues { get; init; }
|
||||
|
||||
/// <summary>Notices écartées parce qu'aucun de leurs auteurs n'est celui demandé.</summary>
|
||||
public int NoticesEcartees { get; init; }
|
||||
|
||||
/// <summary>Vrai si la BnF en annonce plus qu'on n'en a lu : la liste est un extrait.</summary>
|
||||
public bool Tronquee => NoticesAnnoncees > NoticesLues;
|
||||
|
||||
/// <summary>Message à afficher quand la source n'a pas pu être interrogée normalement.</summary>
|
||||
public string? Avertissement { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Shared.Entites;
|
||||
|
||||
/// <summary>
|
||||
/// Un livre que l'on aimerait acquérir. <b>Personnel</b> : chacun sa liste.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Pourquoi une entité séparée plutôt qu'un statut de plus sur <see cref="Livre"/> ?</b>
|
||||
/// Un livre souhaité n'est pas un livre possédé. S'il était une ligne de <c>Livres</c>, il
|
||||
/// entrerait mécaniquement dans le catalogue, dans les compteurs, dans les prêts et dans le
|
||||
/// cache hors-ligne — et il faudrait ajouter « et qui n'est pas souhaité » à <b>chaque</b>
|
||||
/// lecture du catalogue. Un invariant qu'il faut réécrire à chaque requête finit toujours par
|
||||
/// être oublié quelque part ; ici l'inventaire et l'envie ne se croisent tout simplement pas,
|
||||
/// parce qu'ils ne vivent pas dans la même table.
|
||||
/// <para>
|
||||
/// C'est aussi la seule forme qui accepte une envie <b>sans ISBN ni édition arrêtée</b> : on
|
||||
/// souhaite une œuvre (« le dernier Werber »), on possède un exemplaire. Un souhait n'a donc
|
||||
/// ni <see cref="Format"/> ni couverture obligatoire, et ne peut pas être prêté.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// La portée personnelle suit <see cref="StatutLecture"/>, dont la documentation annonçait déjà
|
||||
/// ce voisinage : <see cref="Utilisateur"/> <b>est</b> une frontière, contrairement à
|
||||
/// <see cref="Livre.AjoutePar"/>. On ne lit jamais la liste d'envies de quelqu'un d'autre —
|
||||
/// c'est précisément ce qui permet d'offrir un livre sans vendre la mèche.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class LivreSouhaite
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <c>YNH_USER</c> du propriétaire de l'envie. <b>Frontière</b> : toute lecture filtre dessus.
|
||||
/// </summary>
|
||||
public string Utilisateur { get; set; } = string.Empty;
|
||||
|
||||
public string Titre { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Titre mis à plat, servant à la fois au dédoublonnage et au rapprochement avec le
|
||||
/// catalogue (« ai-je déjà ce livre ? »). Recalculé à chaque écriture.
|
||||
/// </summary>
|
||||
public string TitreNormalise { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Auteur en <b>texte libre</b>, volontairement pas une clé étrangère vers <see cref="Auteur"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// La table <c>Auteurs</c> décrit qui est <i>dans la bibliothèque</i> : y insérer les auteurs
|
||||
/// souhaités les ferait apparaître sur l'écran des auteurs avec « 0 livre », et exposerait la
|
||||
/// liste d'envies d'une personne à tout le foyer — l'inverse de ce qu'on veut. Le texte libre
|
||||
/// survit en outre à la disparition de l'auteur du catalogue.
|
||||
/// </remarks>
|
||||
public string? Auteur { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Auteur mis à plat. <b>Jamais <c>null</c></b> (chaîne vide si l'auteur est inconnu) :
|
||||
/// SQLite considère deux <c>NULL</c> comme distincts, et l'index unique laisserait alors
|
||||
/// passer des doublons d'envies sans auteur.
|
||||
/// </summary>
|
||||
public string AuteurNormalise { get; set; } = string.Empty;
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
|
||||
/// <summary>Année de publication, telle que la source la donne. Texte : la BnF écrit « 2012 », mais aussi « [DL 2012] ».</summary>
|
||||
public string? Annee { get; set; }
|
||||
|
||||
/// <summary>ISBN d'une édition repérée, quand il y en a une. Facultatif : on souhaite une œuvre, pas forcément une édition.</summary>
|
||||
public string? Isbn { get; set; }
|
||||
|
||||
public string? CoverUrl { get; set; }
|
||||
|
||||
/// <summary>Note libre : « demandé à Noël », « en poche seulement », « chez Untel ».</summary>
|
||||
public string? Note { get; set; }
|
||||
|
||||
public DateTime DateAjout { get; set; }
|
||||
|
||||
public void RecalculerFormes()
|
||||
{
|
||||
Titre = Titre.Trim();
|
||||
TitreNormalise = CleOeuvre.Cle(Titre);
|
||||
Auteur = string.IsNullOrWhiteSpace(Auteur) ? null : Auteur.Trim();
|
||||
AuteurNormalise = RapprochementAuteurs.Cle(Auteur);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace MaBibli.Shared.Textes;
|
||||
|
||||
/// <summary>
|
||||
/// Clé d'identification d'une <b>œuvre</b> à partir d'un titre, indépendamment de l'édition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// C'est le rapprochement le moins mauvais dont on dispose, et il faut être clair sur ses limites.
|
||||
/// <para>
|
||||
/// <b>Pourquoi pas l'ISBN ?</b> Parce qu'il désigne une <i>édition</i>, pas une œuvre : le poche
|
||||
/// de 1998, le grand format de 1991 et la réédition de 2015 portent trois ISBN différents pour le
|
||||
/// même roman. Mesuré sur la bibliographie BnF de Bernard Werber : 183 notices de livres pour
|
||||
/// 51 œuvres, chacune revenant en moyenne trois fois sous trois ISBN. Comparer les ISBN
|
||||
/// répondrait donc « vous ne l'avez pas » à propos d'un livre posé sur l'étagère, ce qui est le
|
||||
/// pire résultat possible pour une liste d'envies.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ce que la clé fait.</b> Elle coupe la mention de responsabilité ISBD (« <c> / </c> ») puis
|
||||
/// le sous-titre (« <c> : </c> »), et met le reste à plat. C'est ce qui réunit
|
||||
/// « Les thanatonautes », « Les thanatonautes : roman » et
|
||||
/// « Les thanatonautes / Bernard Werber ».
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ce qu'elle rate</b>, et qu'il ne faut pas prétendre autrement :
|
||||
/// <list type="bullet">
|
||||
/// <item>Deux œuvres distinctes partageant un titre principal sont confondues (« Œuvres »,
|
||||
/// « Nouvelles »). Le risque est borné : la comparaison n'a lieu qu'<b>à auteur donné</b>.</item>
|
||||
/// <item>Un tome n'est pas rapproché de son recueil : « Troisième humanité » et
|
||||
/// « Troisième humanité. Tome 1 » restent deux lignes. Le point n'est pas coupé, faute de savoir
|
||||
/// distinguer « Tome 1 » d'un titre qui contient légitimement un point.</item>
|
||||
/// <item>Un titre retraduit ou changé au fil des rééditions n'est pas rapproché du tout.</item>
|
||||
/// <item>Une œuvre possédée à l'intérieur d'une intégrale n'est pas repérée : l'intégrale a son
|
||||
/// propre titre.</item>
|
||||
/// </list>
|
||||
/// Conséquence assumée : l'écran de bibliographie peut afficher comme « à découvrir » un livre
|
||||
/// déjà présent sous un autre titre. Il ne fait donc que <b>griser</b> ce qu'il reconnaît, sans
|
||||
/// jamais masquer une ligne — un faux négatif se voit et se corrige d'un coup d'œil, alors qu'une
|
||||
/// ligne masquée à tort serait invisible.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class CleOeuvre
|
||||
{
|
||||
/// <summary>Sépare le titre de la mention de responsabilité : « Germinal / Émile Zola ».</summary>
|
||||
private const string SeparateurResponsabilite = " / ";
|
||||
|
||||
/// <summary>Sépare le titre de son sous-titre : « Les thanatonautes : roman ».</summary>
|
||||
private const string SeparateurSousTitre = " : ";
|
||||
|
||||
/// <summary>
|
||||
/// Clé comparable d'un titre : « Les thanatonautes : roman » → <c>les thanatonautes</c>.
|
||||
/// </summary>
|
||||
public static string Cle(string? titre)
|
||||
{
|
||||
var s = titre?.Trim();
|
||||
if (string.IsNullOrEmpty(s))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
s = Tronquer(s, SeparateurResponsabilite);
|
||||
var sansSousTitre = Tronquer(s, SeparateurSousTitre);
|
||||
|
||||
// Un titre qui n'est QUE son sous-titre (« : roman ») ne doit pas se réduire à rien :
|
||||
// mieux vaut alors garder la chaîne entière que produire une clé vide, qui se
|
||||
// confondrait avec toutes les autres clés vides.
|
||||
var retenu = NormalisationTexte.Normaliser(sansSousTitre);
|
||||
return retenu.Length > 0 ? retenu : NormalisationTexte.Normaliser(s);
|
||||
}
|
||||
|
||||
private static string Tronquer(string s, string separateur)
|
||||
{
|
||||
var i = s.IndexOf(separateur, StringComparison.Ordinal);
|
||||
return i >= 0 ? s[..i].Trim() : s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using MaBibli.Api.Services.Catalogue;
|
||||
using MaBibli.Api.Services.Isbn;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Lecture des réponses SRU « par auteur », sur des réponses <b>réelles</b> enregistrées le
|
||||
/// 2026-08-18 (25 notices chacune). Aucun accès réseau.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Les deux fixtures sont choisies pour ce qu'elles ont de gênant :
|
||||
/// <list type="bullet">
|
||||
/// <item><c>werber</c> : bibliographie propre mais très redondante (rééditions en série) ;</item>
|
||||
/// <item><c>zola</c> : bibliographie <b>polluée</b> — l'index auteur de la BnF y mêle les
|
||||
/// ouvrages de sa fille Denise Le Blond-Zola, qui parlent de lui sans qu'il les ait écrits.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public class BnfBibliographieParserTests
|
||||
{
|
||||
private const string Werber = "bnf-auteur-werber.xml";
|
||||
private const string Zola = "bnf-auteur-zola.xml";
|
||||
|
||||
[Fact]
|
||||
public void Lit_le_nombre_de_notices_annonce_par_la_BnF()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
|
||||
// La BnF en annonce bien plus qu'elle n'en renvoie : c'est ce qui permet à l'écran de
|
||||
// dire honnêtement qu'il ne montre qu'un extrait.
|
||||
Assert.True(
|
||||
resultat.NombreAnnonce > resultat.NombreLu,
|
||||
$"annoncé {resultat.NombreAnnonce}, lu {resultat.NombreLu}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retient_les_notices_de_l_auteur_demande()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
|
||||
Assert.NotEmpty(resultat.Notices);
|
||||
Assert.All(
|
||||
resultat.Notices,
|
||||
n => Assert.Contains(n.Auteurs, a => a.Contains("Werber", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Le filtre critique : <c>bib.author all "Émile Zola"</c> rapproche « émile » et « zola »
|
||||
/// sur l'ensemble des auteurs d'une notice, et remonte donc l'œuvre de sa fille.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ecarte_les_ouvrages_ecrits_par_quelqu_un_d_autre()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Zola), "Émile Zola");
|
||||
|
||||
Assert.True(
|
||||
resultat.EcarteesAutreAuteur > 0,
|
||||
"la fixture Zola doit contenir des notices d'un autre auteur, sinon elle ne teste rien");
|
||||
|
||||
// Denise Le Blond-Zola a écrit sur son père ; ses livres ne sont pas de lui.
|
||||
Assert.DoesNotContain(
|
||||
resultat.Notices,
|
||||
n => n.Auteurs.Any(a => a.Contains("Le Blond", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
Assert.All(
|
||||
resultat.Notices,
|
||||
n => Assert.Contains(
|
||||
n.Auteurs,
|
||||
a => MaBibli.Shared.Textes.RapprochementAuteurs.SontLeMemeAuteur(a, "Émile Zola")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tolere_la_forme_bibliographique_du_nom()
|
||||
{
|
||||
// La notice porte « Werber, Bernard (1961-....). Auteur du texte » : c'est le nettoyage
|
||||
// ISBD puis le rapprochement d'auteurs qui doivent le ramener à « Bernard Werber ».
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "werber, bernard");
|
||||
|
||||
Assert.NotEmpty(resultat.Notices);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extrait_l_ISBN_du_champ_identifier()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
|
||||
var avecIsbn = resultat.Notices.Where(n => !string.IsNullOrWhiteSpace(n.Isbn)).ToList();
|
||||
|
||||
Assert.NotEmpty(avecIsbn);
|
||||
|
||||
// Le préfixe « ISBN » du dc:identifier ne doit pas rester collé au numéro.
|
||||
Assert.All(avecIsbn, n => Assert.DoesNotContain("ISBN", n.Isbn!, StringComparison.OrdinalIgnoreCase));
|
||||
Assert.All(avecIsbn, n => Assert.True(n.Isbn!.Length is 10 or 13, n.Isbn));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Coupe_la_mention_de_responsabilite_du_titre()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
|
||||
// « Troisième humanité / Bernard Werber » → « Troisième humanité ».
|
||||
Assert.All(resultat.Notices, n => Assert.DoesNotContain(" / ", n.Titre));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Le regroupement est ce qui rend l'écran utilisable : sans lui, « Les fourmis » occupe
|
||||
/// autant de lignes qu'il y a eu de retirages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Regroupe_les_reeditions_sous_une_seule_oeuvre()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
var oeuvres = ServiceBibliographie.Regrouper(resultat).ToList();
|
||||
|
||||
Assert.True(
|
||||
oeuvres.Count < resultat.Notices.Count,
|
||||
$"{resultat.Notices.Count} notices devraient donner moins de {resultat.Notices.Count} œuvres");
|
||||
|
||||
// Aucune œuvre en double après regroupement.
|
||||
var cles = oeuvres.Select(o => MaBibli.Shared.Textes.CleOeuvre.Cle(o.Titre)).ToList();
|
||||
Assert.Equal(cles.Count, cles.Distinct().Count());
|
||||
|
||||
// Au moins une œuvre a bien réuni plusieurs notices, sinon le test ne prouve rien.
|
||||
Assert.Contains(oeuvres, o => o.NombreEditions > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retient_l_edition_la_plus_ancienne_comme_representant()
|
||||
{
|
||||
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
|
||||
|
||||
foreach (var oeuvre in ServiceBibliographie.Regrouper(resultat).Where(o => o.NombreEditions > 1))
|
||||
{
|
||||
var cle = MaBibli.Shared.Textes.CleOeuvre.Cle(oeuvre.Titre);
|
||||
|
||||
var anneesDuGroupe = resultat.Notices
|
||||
.Where(n => n.CleTitre == cle)
|
||||
.Select(n => ServiceBibliographie.AnneeTriable(n.Annee))
|
||||
.Where(a => a is not null)
|
||||
.Select(a => a!.Value)
|
||||
.ToList();
|
||||
|
||||
if (anneesDuGroupe.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.Equal(anneesDuGroupe.Min(), ServiceBibliographie.AnneeTriable(oeuvre.Annee));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Une_reponse_vide_ne_fait_pas_echouer_la_lecture()
|
||||
{
|
||||
const string vide = """
|
||||
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
|
||||
<srw:numberOfRecords>0</srw:numberOfRecords>
|
||||
</srw:searchRetrieveResponse>
|
||||
""";
|
||||
|
||||
var resultat = BnfBibliographieParser.Parser(vide, "Personne");
|
||||
|
||||
Assert.Empty(resultat.Notices);
|
||||
Assert.Equal(0, resultat.NombreAnnonce);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2012", 2012)]
|
||||
[InlineData("[DL 2012]", 2012)]
|
||||
[InlineData("cop. 1991", 1991)]
|
||||
[InlineData("19..?", null)]
|
||||
[InlineData("", null)]
|
||||
[InlineData(null, null)]
|
||||
public void Extrait_une_annee_triable_des_dates_BnF(string? date, int? attendu) =>
|
||||
Assert.Equal(attendu, ServiceBibliographie.AnneeTriable(date));
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Clé d'œuvre : ce qu'elle réunit, et surtout <b>ce qu'elle rate</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Les cas d'échec sont testés au même titre que les réussites : ils documentent la limite du
|
||||
/// rapprochement, que l'interface annonce à l'utilisateur. Si l'un d'eux se met à passer un
|
||||
/// jour, c'est que la règle a changé — et le message affiché doit changer avec.
|
||||
/// </remarks>
|
||||
public class CleOeuvreTests
|
||||
{
|
||||
[Theory]
|
||||
// Les trois formes rencontrées pour un même roman de Werber.
|
||||
[InlineData("Les thanatonautes", "Les thanatonautes : roman")]
|
||||
[InlineData("Les thanatonautes", "Les thanatonautes / Bernard Werber")]
|
||||
[InlineData("Les thanatonautes : roman", "Les thanatonautes / Bernard Werber")]
|
||||
// Casse et accents, comme partout ailleurs dans le projet.
|
||||
[InlineData("Germinal", "GERMINAL")]
|
||||
public void Reunit_les_variantes_d_un_meme_titre(string a, string b) =>
|
||||
Assert.Equal(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
|
||||
|
||||
[Theory]
|
||||
[InlineData("Germinal", "La bête humaine")]
|
||||
// Un tome n'est pas rapproché de l'œuvre entière : le point n'est pas coupé.
|
||||
[InlineData("Troisième humanité", "Troisième humanité. Tome 1")]
|
||||
public void Distingue_deux_oeuvres_differentes(string a, string b) =>
|
||||
Assert.NotEqual(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
|
||||
|
||||
/// <summary>
|
||||
/// Limite assumée : deux œuvres distinctes partageant leur titre principal se confondent.
|
||||
/// Le risque est borné, la comparaison n'ayant lieu qu'à auteur donné.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Confond_deux_oeuvres_partageant_le_titre_principal() =>
|
||||
Assert.Equal(
|
||||
CleOeuvre.Cle("Nouvelles : premier recueil"),
|
||||
CleOeuvre.Cle("Nouvelles : second recueil"));
|
||||
|
||||
/// <summary>
|
||||
/// Limite <b>héritée</b> de <see cref="NormalisationTexte"/>, pas propre à la clé d'œuvre :
|
||||
/// la ligature « œ » n'est pas ramenée à « oe ».
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>NormalisationTexte</c> décompose en NFD, qui sépare les accents mais laisse les
|
||||
/// ligatures intactes — seul NFKD les défait. Conséquence concrète : <i>L'Œuvre</i> de Zola,
|
||||
/// que la BnF écrit avec la ligature, n'est pas rapproché d'un « L'oeuvre » saisi au clavier.
|
||||
/// <para>
|
||||
/// Le défaut ne vient pas d'ici et ne se corrige pas ici : la même fonction alimente la
|
||||
/// recherche du catalogue et la clé unique des auteurs, et en changer déplacerait ces deux
|
||||
/// invariants. Consigné dans IDEES.md ; ce test existe pour que la limite soit visible plutôt
|
||||
/// que découverte en usage.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void Ne_reduit_pas_la_ligature_oe() =>
|
||||
Assert.NotEqual(CleOeuvre.Cle("L'Œuvre"), CleOeuvre.Cle("L'oeuvre"));
|
||||
|
||||
/// <summary>
|
||||
/// Limite assumée : un titre retraduit n'est pas rapproché du tout.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ne_rapproche_pas_un_titre_retraduit() =>
|
||||
Assert.NotEqual(CleOeuvre.Cle("Le Seigneur des anneaux"), CleOeuvre.Cle("The Lord of the Rings"));
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Rend_une_cle_vide_pour_une_saisie_vide(string? titre) =>
|
||||
Assert.Equal(string.Empty, CleOeuvre.Cle(titre));
|
||||
|
||||
/// <summary>
|
||||
/// Un titre réduit à son seul sous-titre ne doit pas produire une clé vide : elle se
|
||||
/// confondrait avec toutes les autres clés vides.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ne_vide_pas_un_titre_qui_commence_par_le_separateur() =>
|
||||
Assert.NotEqual(string.Empty, CleOeuvre.Cle(": roman"));
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using System.Text;
|
||||
using MaBibli.Api.Services.Souhaits;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Contenu <b>exact</b> des fichiers d'export. Fonctions pures : rien à simuler.
|
||||
/// </summary>
|
||||
public class ExportSouhaitsTests
|
||||
{
|
||||
private static readonly DateTime Jour = new(2026, 8, 18, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static SouhaitDto Souhait(
|
||||
int id, string titre, string? auteur = null, string? editeur = null,
|
||||
string? annee = null, string? isbn = null, string? note = null) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
Titre = titre,
|
||||
Auteur = auteur,
|
||||
Editeur = editeur,
|
||||
Annee = annee,
|
||||
Isbn = isbn,
|
||||
Note = note,
|
||||
DateAjout = Jour,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Le_texte_groupe_les_livres_par_auteur()
|
||||
{
|
||||
var texte = ExportSouhaits.Texte(
|
||||
[
|
||||
Souhait(1, "La prophétie des abeilles", "Bernard Werber"),
|
||||
Souhait(2, "Germinal", "Émile Zola"),
|
||||
Souhait(3, "Les fourmis", "Bernard Werber"),
|
||||
],
|
||||
"Mathieu",
|
||||
Jour);
|
||||
|
||||
Assert.Contains("Liste d'envies de Mathieu — 18/08/2026", texte);
|
||||
Assert.Contains("3 livres", texte);
|
||||
|
||||
// Un seul en-tête par auteur, et ses deux titres regroupés dessous.
|
||||
var lignes = texte.Split('\n');
|
||||
Assert.Single(lignes, l => l == "Bernard Werber");
|
||||
|
||||
var debutWerber = Array.IndexOf(lignes, "Bernard Werber");
|
||||
Assert.Equal(" - La prophétie des abeilles", lignes[debutWerber + 1]);
|
||||
Assert.Equal(" - Les fourmis", lignes[debutWerber + 2]);
|
||||
|
||||
// Werber avant Zola : les auteurs sont classés alphabétiquement.
|
||||
Assert.True(debutWerber < Array.IndexOf(lignes, "Émile Zola"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_texte_range_les_livres_sans_auteur_en_dernier()
|
||||
{
|
||||
var texte = ExportSouhaits.Texte(
|
||||
[Souhait(1, "Titre orphelin"), Souhait(2, "Germinal", "Émile Zola")],
|
||||
null,
|
||||
Jour);
|
||||
|
||||
Assert.True(texte.IndexOf("Émile Zola", StringComparison.Ordinal)
|
||||
< texte.IndexOf("Auteur non précisé", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_texte_porte_les_precisions_utiles_en_librairie()
|
||||
{
|
||||
var texte = ExportSouhaits.Texte(
|
||||
[Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885", "2253004227", "en poche seulement")],
|
||||
null,
|
||||
Jour);
|
||||
|
||||
Assert.Contains(" - Germinal (Le Livre de poche, 1885, ISBN 2253004227)", texte);
|
||||
Assert.Contains(" en poche seulement", texte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_texte_le_dit_quand_la_liste_est_vide()
|
||||
{
|
||||
var texte = ExportSouhaits.Texte([], "Mathieu", Jour);
|
||||
|
||||
Assert.Contains("(aucun livre dans la liste)", texte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_csv_porte_un_en_tete_et_une_ligne_par_envie()
|
||||
{
|
||||
var csv = ExportSouhaits.Csv([Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885")]);
|
||||
|
||||
var lignes = csv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Assert.Equal("Titre;Auteur;Éditeur;Année;ISBN;Note;Ajouté le", lignes[0]);
|
||||
Assert.Equal("Germinal;Émile Zola;Le Livre de poche;1885;;;2026-08-18", lignes[1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sans échappement, une note contenant le séparateur décalerait toutes les colonnes
|
||||
/// suivantes — et une note est du texte parfaitement libre.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Le_csv_echappe_le_separateur_et_les_guillemets()
|
||||
{
|
||||
var csv = ExportSouhaits.Csv(
|
||||
[Souhait(1, "Titre; piégé", note: "il a dit \"oui\"")]);
|
||||
|
||||
var ligne = csv.Split("\r\n")[1];
|
||||
|
||||
Assert.StartsWith("\"Titre; piégé\";", ligne);
|
||||
Assert.Contains("\"il a dit \"\"oui\"\"\"", ligne);
|
||||
|
||||
// Sept colonnes une fois l'échappement respecté, pas huit.
|
||||
Assert.Equal(7, ColonnesCsv(ligne).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_csv_echappe_les_sauts_de_ligne()
|
||||
{
|
||||
var csv = ExportSouhaits.Csv([Souhait(1, "Titre", note: "ligne 1\nligne 2")]);
|
||||
|
||||
Assert.Contains("\"ligne 1\nligne 2\"", csv);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sans BOM, Excel affiche « Émile Zola » — sur un fonds français, à peu près chaque ligne.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Le_bom_precede_le_csv()
|
||||
{
|
||||
var octets = ExportSouhaits.Bom
|
||||
.Concat(Encoding.UTF8.GetBytes(ExportSouhaits.Csv([Souhait(1, "Émile")])))
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal([0xEF, 0xBB, 0xBF], octets[..3]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(FormatExportSouhaits.Texte, "liste-envies-2026-08-18.txt", "text/plain; charset=utf-8")]
|
||||
[InlineData(FormatExportSouhaits.Csv, "liste-envies-2026-08-18.csv", "text/csv; charset=utf-8")]
|
||||
public void Le_fichier_est_date_et_typé(FormatExportSouhaits format, string nom, string mime)
|
||||
{
|
||||
Assert.Equal(nom, ExportSouhaits.NomFichier(format, Jour));
|
||||
Assert.Equal(mime, ExportSouhaits.TypeMime(format));
|
||||
}
|
||||
|
||||
/// <summary>Découpe une ligne CSV en respectant les guillemets (lecteur minimal, pour le test).</summary>
|
||||
private static List<string> ColonnesCsv(string ligne)
|
||||
{
|
||||
var colonnes = new List<string>();
|
||||
var courante = new StringBuilder();
|
||||
var dansGuillemets = false;
|
||||
|
||||
for (var i = 0; i < ligne.Length; i++)
|
||||
{
|
||||
var c = ligne[i];
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
if (dansGuillemets && i + 1 < ligne.Length && ligne[i + 1] == '"')
|
||||
{
|
||||
courante.Append('"');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
dansGuillemets = !dansGuillemets;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ExportSouhaits.SeparateurCsv && !dansGuillemets)
|
||||
{
|
||||
colonnes.Add(courante.ToString());
|
||||
courante.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
courante.Append(c);
|
||||
}
|
||||
|
||||
colonnes.Add(courante.ToString());
|
||||
return colonnes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/" xmlns="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:ixm="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:mn="http://catalogue.bnf.fr/namespaces/motsnotices" xmlns:sd="http://www.loc.gov/zing/srw/diagnostic/">
|
||||
<srw:version>1.2</srw:version>
|
||||
<srw:echoedSearchRetrieveRequest>
|
||||
<srw:version>1.2</srw:version>
|
||||
<srw:query>bib.author all "Bernard Werber"</srw:query>
|
||||
</srw:echoedSearchRetrieveRequest>
|
||||
<srw:numberOfRecords>203</srw:numberOfRecords>
|
||||
<srw:records>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43683323s</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb43683323s</srw:recordIdentifier>
|
||||
<srw:recordPosition>1</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20131002</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20131002</ixm:attr>
|
||||
<mn:score>12.24525</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43745025g</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2013</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb43745025g</srw:recordIdentifier>
|
||||
<srw:recordPosition>2</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20140116</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20140116</ixm:attr>
|
||||
<mn:score>12.24525</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb42777895t</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb42777895t</srw:recordIdentifier>
|
||||
<srw:recordPosition>3</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20121024</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20121024</ixm:attr>
|
||||
<mn:score>12.24525</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43683332r</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb43683332r</srw:recordIdentifier>
|
||||
<srw:recordPosition>4</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20131002</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20131002</ixm:attr>
|
||||
<mn:score>12.2451105</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb39269846w</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>2004</dc:date> <dc:format>24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb39269846w</srw:recordIdentifier>
|
||||
<srw:recordPosition>5</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20041103</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20071205</ixm:attr>
|
||||
<mn:score>12.2451105</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb40039497k</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2005</dc:date> <dc:format>21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb40039497k</srw:recordIdentifier>
|
||||
<srw:recordPosition>6</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20050922</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20081030</ixm:attr>
|
||||
<mn:score>12.2451105</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb47615775q</dc:identifier> <dc:title>L'encyclopédie du savoir relatif et absolu / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Albin Michel (Paris)</dc:publisher> <dc:date>2018</dc:date> <dc:format>24 et 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb47615775q</srw:recordIdentifier>
|
||||
<srw:recordPosition>7</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20241211</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20241211</ixm:attr>
|
||||
<mn:score>12.244974</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb39900329q</dc:identifier> <dc:title>Nous, les dieux : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2004</dc:date> <dc:format>25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb39900329q</srw:recordIdentifier>
|
||||
<srw:recordPosition>8</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20050106</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20071205</ixm:attr>
|
||||
<mn:score>12.244974</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb444898718</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Livre de poche (Paris)</dc:publisher> <dc:date>2015</dc:date> <dc:description>Collection : Le livre de poche</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb444898718</srw:recordIdentifier>
|
||||
<srw:recordPosition>9</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20160118</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20160118</ixm:attr>
|
||||
<mn:score>12.244841</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb409564505</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Librairie générale française (Paris)</dc:publisher> <dc:date>2006</dc:date> <dc:description>Collection : Le livre de poche</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb409564505</srw:recordIdentifier>
|
||||
<srw:recordPosition>10</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20061215</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20061215</ixm:attr>
|
||||
<mn:score>12.244579</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35551566h</dc:identifier> <dc:title>Le jour des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1992</dc:date> <dc:format>463 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35551566h</srw:recordIdentifier>
|
||||
<srw:recordPosition>11</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19930209</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19930223</ixm:attr>
|
||||
<mn:score>12.244083</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb357335589</dc:identifier> <dc:title>Les thanatonautes / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1994</dc:date> <dc:identifier>ISBN 2724280946</dc:identifier> <dc:format>450 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb357335589</srw:recordIdentifier>
|
||||
<srw:recordPosition>12</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19950130</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19950209</ixm:attr>
|
||||
<mn:score>12.243847</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35720318k</dc:identifier> <dc:title>Les thanatonautes : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1994</dc:date> <dc:format>450 p. : couv. ill. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35720318k</srw:recordIdentifier>
|
||||
<srw:recordPosition>13</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19941114</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19941201</ixm:attr>
|
||||
<mn:score>12.243847</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43699999b</dc:identifier> <dc:title>Les fourmis / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Livre de poche jeunesse (Paris)</dc:publisher> <dc:date>2013</dc:date> <dc:description>Collection : Le livre de poche. Jeunesse. Fictions</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb43699999b</srw:recordIdentifier>
|
||||
<srw:recordPosition>14</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20131025</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20210730</ixm:attr>
|
||||
<mn:score>12.243847</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35828640p</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:format>533 p. : couv. ill. en coul. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35828640p</srw:recordIdentifier>
|
||||
<srw:recordPosition>15</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19960806</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19960906</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb36193471k</dc:identifier> <dc:title>Le livre du voyage / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1997</dc:date> <dc:format>127 p. : couv. ill. en coul. ; 22 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb36193471k</srw:recordIdentifier>
|
||||
<srw:recordPosition>16</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19980108</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19980206</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb358187920</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:identifier>ISBN 2226086366</dc:identifier> <dc:format>533 p. : couv. ill. en coul. ; 24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb358187920</srw:recordIdentifier>
|
||||
<srw:recordPosition>17</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19960620</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19960712</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb361686335</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1997</dc:date> <dc:identifier>ISBN 2744104000</dc:identifier> <dc:format>533 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb361686335</srw:recordIdentifier>
|
||||
<srw:recordPosition>18</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970611</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19970711</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35492416k</dc:identifier> <dc:title>Les fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1991</dc:date> <dc:identifier>ISBN 2724265696</dc:identifier> <dc:format>351 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35492416k</srw:recordIdentifier>
|
||||
<srw:recordPosition>19</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19920127</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20160128</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35540108s</dc:identifier> <dc:title>Le jour des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>1992</dc:date> <dc:identifier>ISBN 2226061185</dc:identifier> <dc:format>463 p. : couv. ill. en coul. ; 24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35540108s</srw:recordIdentifier>
|
||||
<srw:recordPosition>20</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19921204</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19921222</ixm:attr>
|
||||
<mn:score>12.243619</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb370268608</dc:identifier> <dc:title>Le livre du voyage / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1998</dc:date> <dc:identifier>ISBN 2744115509</dc:identifier> <dc:format>127 p. : jaquette ill. ; 22 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb370268608</srw:recordIdentifier>
|
||||
<srw:recordPosition>21</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19980921</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19990402</ixm:attr>
|
||||
<mn:score>12.243397</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb42606801s</dc:identifier> <dc:title>Le rire du Cyclope / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2011</dc:date> <dc:identifier>ISBN 9782298045680</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298045680</dc:description> <dc:format>1 vol. (816 p.) : jaquette ill. en coul. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb42606801s</srw:recordIdentifier>
|
||||
<srw:recordPosition>22</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20120221</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20120329</ixm:attr>
|
||||
<mn:score>12.243397</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45372631j</dc:identifier> <dc:title>Demain les chats : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éditions France loisirs (Paris)</dc:publisher> <dc:date>2017</dc:date> <dc:identifier>ISBN 9782298122015</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298122015</dc:description> <dc:format>1 vol. (335 p.) ; 20 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb45372631j</srw:recordIdentifier>
|
||||
<srw:recordPosition>23</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20171018</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20171207</ixm:attr>
|
||||
<mn:score>12.243397</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35807212p</dc:identifier> <dc:title>Les thanatonautes : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Librairie générale française (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:description>Collection : Le livre de poche ; 13922</dc:description> <dc:identifier>ISBN 225313922X</dc:identifier> <dc:format>502 p. : couv. ill. ; 18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35807212p</srw:recordIdentifier>
|
||||
<srw:recordPosition>24</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19960417</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19960517</ixm:attr>
|
||||
<mn:score>12.243397</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45159572s</dc:identifier> <dc:title>Demain les chats : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éditions France loisirs (Paris)</dc:publisher> <dc:date>2016</dc:date> <dc:identifier>ISBN 9782298119732</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298119732</dc:description> <dc:format>1 vol. (307 p.) ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb45159572s</srw:recordIdentifier>
|
||||
<srw:recordPosition>25</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20161117</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20161222</ixm:attr>
|
||||
<mn:score>12.243397</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
</srw:records>
|
||||
<srw:nextRecordPosition>26</srw:nextRecordPosition>
|
||||
</srw:searchRetrieveResponse>
|
||||
@@ -0,0 +1,361 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/" xmlns="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:ixm="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:mn="http://catalogue.bnf.fr/namespaces/motsnotices" xmlns:sd="http://www.loc.gov/zing/srw/diagnostic/">
|
||||
<srw:version>1.2</srw:version>
|
||||
<srw:echoedSearchRetrieveRequest>
|
||||
<srw:version>1.2</srw:version>
|
||||
<srw:query>bib.author all "Emile Zola"</srw:query>
|
||||
</srw:echoedSearchRetrieveRequest>
|
||||
<srw:numberOfRecords>2692</srw:numberOfRecords>
|
||||
<srw:records>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb32359518t</dc:identifier> <dc:title>Zola und die Tiere... / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>I. Graetz-Verlag (Berlin-Charlottenburg)</dc:publisher> <dc:date>1933</dc:date> <dc:description>Collection : Entr'act-Bücherei ; 3</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>In-8°, 40 p., pl.</dc:format> <dc:language>ger</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb32359518t</srw:recordIdentifier>
|
||||
<srw:recordPosition>1</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20191219</ixm:attr>
|
||||
<mn:score>11.365902</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb370961796</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>B. Grasset (Paris)</dc:publisher> <dc:date>2000</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 2246172632</dc:identifier> <dc:format>280 p. : couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb370961796</srw:recordIdentifier>
|
||||
<srw:recordPosition>2</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20000322</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190914</ixm:attr>
|
||||
<mn:score>11.36502</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb38668184t</dc:identifier> <dc:title>Zola et Cézanne d'après une correspondance retrouvée</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:date>1931</dc:date> <dc:format>Pag. 39-58 ; In-8 °</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb38668184t</srw:recordIdentifier>
|
||||
<srw:recordPosition>3</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20041209</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20041209</ixm:attr>
|
||||
<mn:score>11.364924</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb33073483k</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Cercle du bibliophile (Levallois-Perret)</dc:publisher> <dc:date>1966</dc:date> <dc:description>Collection : Cercle du bibliophile</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>1 vol. (290 p.) : pl., fac-sim. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb33073483k</srw:recordIdentifier>
|
||||
<srw:recordPosition>4</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20130418</ixm:attr>
|
||||
<mn:score>11.364755</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34870862h</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>B. Grasset (Paris)</dc:publisher> <dc:date>1986</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 2246172624</dc:identifier> <dc:format>289 p. : couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb34870862h</srw:recordIdentifier>
|
||||
<srw:recordPosition>5</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19871004</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190915</ixm:attr>
|
||||
<mn:score>11.36381</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb32359517g</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Fasquelle, éditeur (Paris)</dc:publisher> <dc:date>1931</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>1 vol. (266 p. et portrait) ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb32359517g</srw:recordIdentifier>
|
||||
<srw:recordPosition>6</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20170503</ixm:attr>
|
||||
<mn:score>11.363017</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb386685362</dc:identifier> <dc:title>Paul Alexis, ami des peintres, bohême et critique d'art</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Le mercure de France (Paris)</dc:publisher> <dc:date>1939</dc:date> <dc:subject xml:lang="fre">Alexis, Paul (1847-1901)</dc:subject> <dc:format>1 pièce (paginé 293-303) ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb386685362</srw:recordIdentifier>
|
||||
<srw:recordPosition>7</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20041209</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20070301</ixm:attr>
|
||||
<mn:score>11.362901</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45696031s</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Bernard Grasset (Paris)</dc:publisher> <dc:date>2019</dc:date> <dc:description>Collection : Les cahiers rouges</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 9782246820376</dc:identifier> <dc:description>Code à barres commercial : EAN 9782246820376</dc:description> <dc:format>1 vol. (299 p.) ; 19 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb45696031s</srw:recordIdentifier>
|
||||
<srw:recordPosition>8</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20190326</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190914</ixm:attr>
|
||||
<mn:score>11.361762</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34430876p</dc:identifier> <dc:title>Bulletin de la Société littéraire des amis d'Émile Zola</dc:title> <dc:creator>Société littéraire des amis d'Émile Zola. Auteur du texte</dc:creator> <dc:publisher>[s.n.] (Paris)</dc:publisher> <dc:date>1922-1938</dc:date> <dc:identifier>ISSN 11536284</dc:identifier> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Naturalisme (mouvement littéraire) -- Critique et interprétation</dc:subject> <dc:language>fre</dc:language> <dc:type xml:lang="fre">publication en série imprimée</dc:type> <dc:type xml:lang="eng">printed serial</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb34430876p</srw:recordIdentifier>
|
||||
<srw:recordPosition>9</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19900919</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20210317</ixm:attr>
|
||||
<mn:score>11.290642</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb343484129</dc:identifier> <dc:title>Les Cahiers naturalistes (Paris)</dc:title> <dc:title>Les Cahiers naturalistes : bulletin officiel de la Société littéraire des amis d'Emile Zola</dc:title> <dc:creator>Société littéraire des amis d'Émile Zola. Auteur du texte / Autrice du texte</dc:creator> <dc:publisher>Grasset-Fasquelle (Paris)</dc:publisher> <dc:date>1955</dc:date> <dc:identifier>ISSN 00080365</dc:identifier> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Naturalisme (mouvement littéraire) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Dreyfus, Affaire (1894-1906)</dc:subject> <dc:language>fre</dc:language> <dc:type xml:lang="fre">publication en série imprimée</dc:type> <dc:type xml:lang="eng">printed serial</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb343484129</srw:recordIdentifier>
|
||||
<srw:recordPosition>10</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19860728</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190919</ixm:attr>
|
||||
<mn:score>11.28979</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb323595164</dc:identifier> <dc:title>Émile Zola / par Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>F. Bernouard (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>In-8°, 38 p., couv. en coul.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb323595164</srw:recordIdentifier>
|
||||
<srw:recordPosition>11</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20130418</ixm:attr>
|
||||
<mn:score>11.288839</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb320764010</dc:identifier> <dc:title>Formes mortelles du botulisme / Dr François Émile-Zola,...</dc:title> <dc:creator>Émile-Zola, François (1917-1989). Auteur du texte</dc:creator> <dc:date>1946</dc:date> <dc:format>92 p. ; In-8° (245 x 160)</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb320764010</srw:recordIdentifier>
|
||||
<srw:recordPosition>12</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20180606</ixm:attr>
|
||||
<mn:score>11.266872</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35887568s</dc:identifier> <dc:title>Réponse à la pl ̤. de la R ̤. L. les Jacobins sur les recrutement, discipline et régénération maç ̤. ; (et) Rapport sur l'amnistie générale... / L ̤. Emile Zola n° 382</dc:title> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>l'Emancipatrice (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:format>12 p. ; 27 cm</dc:format> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35887568s</srw:recordIdentifier>
|
||||
<srw:recordPosition>13</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19901220</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19901220</ixm:attr>
|
||||
<mn:score>11.172066</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35311116p</dc:identifier> <dc:title>La Question de l'imprimerie mac ̤. : rapport présenté... / à la L ̤. Emile Zola... ; [par le] F ̤. Frédéric Tizorin,..</dc:title> <dc:contributor>Tizorin, Frédéric. Rédacteur</dc:contributor> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>l'Emancipatrice (Paris)</dc:publisher> <dc:date>1910</dc:date> <dc:format>15 p. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35311116p</srw:recordIdentifier>
|
||||
<srw:recordPosition>14</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19910212</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20151202</ixm:attr>
|
||||
<mn:score>11.106373</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb30082301p</dc:identifier> <dc:title>Lycée de Rennes. Le centenaire de Michelet, discours prononcé dans la salle des fêtes du lycée, le 13 juillet 1898 / par M. Benaerts,...</dc:title> <dc:creator>Benaerts, Louis (1868-1941). Auteur du texte</dc:creator> <dc:contributor>Lycée et collège Émile Zola (Rennes). Éditeur scientifique</dc:contributor> <dc:publisher>impr. des Arts et manufactures (Rennes)</dc:publisher> <dc:date>1898</dc:date> <dc:subject xml:lang="fre">Michelet, Jules (1798-1874) -- Anniversaires</dc:subject> <dc:format>In-8° , 35 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb30082301p</srw:recordIdentifier>
|
||||
<srw:recordPosition>15</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19950501</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20180314</ixm:attr>
|
||||
<mn:score>10.988168</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb33902955k</dc:identifier> <dc:title>Rapport sur la création de cours obligatoires d'instruction et d'éducation pour l'adolescence, par le F @ Henry-Marx,... / G @ L @ D @ F @ L @ Emile-Zola</dc:title> <dc:creator>Marx, Henry. Auteur du texte</dc:creator> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>Impr. L'Emancipatrice (Paris)</dc:publisher> <dc:date>1912</dc:date> <dc:format>In-8°, 11 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb33902955k</srw:recordIdentifier>
|
||||
<srw:recordPosition>16</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20200513</ixm:attr>
|
||||
<mn:score>10.98049</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb412345498</dc:identifier> <dc:title>Mes étés à Brienne / Brigitte Émile-Zola ; préface d'Alain Pagès...</dc:title> <dc:creator>Émile-Zola, Brigitte (1944-....). Auteur du texte</dc:creator> <dc:contributor>Pagès, Alain (1950-....). Préfacier</dc:contributor> <dc:publisher>Éd. du Frisson esthétique (agneaux)</dc:publisher> <dc:date>2008</dc:date> <dc:description>Collection : Collection Sources</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Famille</dc:subject> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Résidences et lieux familiers -- Brienne-le-Château (Aube, France)</dc:subject> <dc:identifier>ISBN 9782917440001</dc:identifier> <dc:description>Code à barres commercial : EAN 9782917440001</dc:description> <dc:format>1 vol. (189 p.) : ill., couv. ill. ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb412345498</srw:recordIdentifier>
|
||||
<srw:recordPosition>17</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20080404</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20230110</ixm:attr>
|
||||
<mn:score>10.976976</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb31241888c</dc:identifier> <dc:title>Université de France. Collège royal de Rennes. Discours prononcé à la distribution solennelle des prix, le 13 août 1845. [Signé : Antonin Rondelet]</dc:title> <dc:creator>Rondelet, Antonin (1823-1893). Auteur du texte</dc:creator> <dc:contributor>Lycée et collège Émile Zola (Rennes). Éditeur scientifique</dc:contributor> <dc:publisher>impr. de L. Boitel (Lyon)</dc:publisher> <dc:date>1845</dc:date> <dc:format>In-8° , 15 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb31241888c</srw:recordIdentifier>
|
||||
<srw:recordPosition>18</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19970701</ixm:attr>
|
||||
<mn:score>10.89491</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb452689930</dc:identifier> <dc:title>Contes à Ninon et Nouveaux contes à Ninon / Émile Zola... ; précédés d'une Vie de l'auteur par Denise Le Blond-Zola</dc:title> <dc:creator>Zola, Émile (1840-1902). Auteur du texte</dc:creator> <dc:contributor>Le Blond-Zola, Denise (1889-1942). Préfacier</dc:contributor> <dc:publisher>typogr. François Bernouard (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:format>1 vol. ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb452689930</srw:recordIdentifier>
|
||||
<srw:recordPosition>19</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20170511</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20170511</ixm:attr>
|
||||
<mn:score>10.85401</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34978585f</dc:identifier> <dc:title>Zola et l'affiche : exposition du 5 octobre 1986 au 29 mars 1987, Maison d'Émile Zola...</dc:title> <dc:contributor>Musée Émile Zola (Médan, Yvelines). Collaborateur</dc:contributor> <dc:publisher>Association du Musée Émile Zola (Médan)</dc:publisher> <dc:date>1986</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Illustrations</dc:subject> <dc:subject xml:lang="fre">Affiches de cinéma -- France</dc:subject> <dc:subject xml:lang="fre">Roman français -- Affiches</dc:subject> <dc:description>[Exposition. Médan. 1986/1987]</dc:description> <dc:format>19 f.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb34978585f</srw:recordIdentifier>
|
||||
<srw:recordPosition>20</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19881216</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20010220</ixm:attr>
|
||||
<mn:score>10.828315</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb346101384</dc:identifier> <dc:title>"Amoco Cadiz" / Émile Zola</dc:title> <dc:creator>Zola, Émile (auteur prétendu). Auteur du texte</dc:creator> <dc:publisher>Baudouin (Paris)</dc:publisher> <dc:date>1978</dc:date> <dc:format>Non paginé ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb346101384</srw:recordIdentifier>
|
||||
<srw:recordPosition>21</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19860717</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20100925</ixm:attr>
|
||||
<mn:score>10.777203</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43908217n</dc:identifier> <dc:title>Lettres à Alexandrine : 1876-1901 / Émile Zola ; édition établie, présentée et annotée par Brigitte Émile-Zola et Alain Pagès ; avec la collaboration de Céline Grenaud-Tostain, Sophie Guermès, Jean-Sébastien Macke... [et al.]</dc:title> <dc:creator>Zola, Émile (1840-1902). Auteur du texte</dc:creator> <dc:contributor>Émile-Zola, Brigitte (1944-....). Éditeur scientifique</dc:contributor> <dc:contributor>Pagès, Alain (1950-....). Éditeur scientifique</dc:contributor> <dc:contributor>Zola, Alexandrine (1839-1925). Destinataire de lettres</dc:contributor> <dc:publisher>Gallimard (Paris)</dc:publisher> <dc:date>2014</dc:date> <dc:identifier>ISBN 9782070139217</dc:identifier> <dc:description>Code à barres commercial : EAN 9782070139217</dc:description> <dc:format>1 vol. (813 p.) ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb43908217n</srw:recordIdentifier>
|
||||
<srw:recordPosition>22</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">20141003</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20250612</ixm:attr>
|
||||
<mn:score>10.722156</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb332677407</dc:identifier> <dc:title>Catalogue des ouvrages légués par M. Saint-Albin Reynaud,...</dc:title> <dc:contributor>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Éditeur scientifique</dc:contributor> <dc:publisher>imp. de Grollier (Montpellier)</dc:publisher> <dc:date>1881</dc:date> <dc:description>[Collection. Saint-Albin Reynaud]</dc:description> <dc:format>In-8°</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb332677407</srw:recordIdentifier>
|
||||
<srw:recordPosition>23</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19970701</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">19970701</ixm:attr>
|
||||
<mn:score>10.693977</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb349977019</dc:identifier> <dc:title>Les Fables de La Fontaine : [exposition], Musée Fabre, 2 mars-15 avril 1984, Ville de Montpellier, Bibliothèque municipale</dc:title> <dc:creator>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Auteur du texte</dc:creator> <dc:publisher>Bibliothèque municipale (Montpellier)</dc:publisher> <dc:date>1984</dc:date> <dc:subject xml:lang="fre">La Fontaine, Jean de (1621-1695). Fables</dc:subject> <dc:description>[Exposition. Montpellier. 1984]</dc:description> <dc:format>[50] p. : ill., couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb349977019</srw:recordIdentifier>
|
||||
<srw:recordPosition>24</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19890214</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190912</ixm:attr>
|
||||
<mn:score>10.693712</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
<srw:record>
|
||||
<srw:recordSchema>dc</srw:recordSchema>
|
||||
<srw:recordPacking>xml</srw:recordPacking>
|
||||
<srw:recordData>
|
||||
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35749705x</dc:identifier> <dc:title>Rabelais à la Bibliothèque municipale : [coffret de 16 cartes postales] / Bibliothèque municipale</dc:title> <dc:creator>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Auteur du texte</dc:creator> <dc:publisher>Bibliothèque municipale (Montpellier)</dc:publisher> <dc:date>1994</dc:date> <dc:subject xml:lang="fre">Cavalier, Calixte (1820-1888) -- Bibliothèque</dc:subject> <dc:subject xml:lang="fre">Rabelais, François (1494?-1553)</dc:subject> <dc:format>16 cartes postales dans un coffret : ill. en noir et en coul. ; 16 cm + avec une notice introductive</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
|
||||
</oai_dc:dc> </srw:recordData>
|
||||
<srw:recordIdentifier>ark:/12148/cb35749705x</srw:recordIdentifier>
|
||||
<srw:recordPosition>25</srw:recordPosition>
|
||||
<srw:extraRecordData>
|
||||
<ixm:attr name="CreationDate">19950524</ixm:attr>
|
||||
<ixm:attr name="LastModificationDate">20190919</ixm:attr>
|
||||
<mn:score>10.693217</mn:score>
|
||||
</srw:extraRecordData>
|
||||
</srw:record>
|
||||
</srw:records>
|
||||
<srw:nextRecordPosition>26</srw:nextRecordPosition>
|
||||
</srw:searchRetrieveResponse>
|
||||
@@ -24,6 +24,14 @@ public class IsbnLookupServiceTests
|
||||
IsbnInterroges.Add(isbn);
|
||||
return Task.FromResult((Reponse(isbn), Avertissement));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient
|
||||
/// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests.
|
||||
/// </summary>
|
||||
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
|
||||
string auteur, CancellationToken ct = default) =>
|
||||
Task.FromResult((new ResultatBibliographie(), (string?)null));
|
||||
}
|
||||
|
||||
private sealed class OpenLibraryFake : IOpenLibraryClient
|
||||
@@ -181,5 +189,13 @@ public class IsbnLookupServiceTests
|
||||
DerniereCouverture = urlCouverture;
|
||||
return Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient
|
||||
/// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests.
|
||||
/// </summary>
|
||||
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
|
||||
string auteur, CancellationToken ct = default) =>
|
||||
Task.FromResult((new ResultatBibliographie(), (string?)null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
using MaBibli.Api.Data;
|
||||
using MaBibli.Api.Services.Catalogue;
|
||||
using MaBibli.Api.Services.Isbn;
|
||||
using MaBibli.Api.Services.Souhaits;
|
||||
using MaBibli.Shared.Catalogue;
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Entites;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Liste d'envies, sur une base SQLite <b>en mémoire</b> : l'unicité « une œuvre par personne »
|
||||
/// n'existe qu'en base, elle ne se vérifierait pas sur un faux dépôt.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// L'invariant central testé ici est la <b>portée personnelle</b> : deux utilisateurs ne doivent
|
||||
/// jamais se voir. C'est l'exact contraire du catalogue, commun au foyer — et une régression y
|
||||
/// serait discrète, puisque tout continuerait de « marcher » avec un seul compte.
|
||||
/// </remarks>
|
||||
public class ServiceSouhaitsTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connexion;
|
||||
private readonly MaBibliDbContext _db;
|
||||
private readonly ServiceSouhaits _souhaits;
|
||||
private readonly ServiceCatalogue _catalogue;
|
||||
|
||||
private const string Mathieu = "mathieu";
|
||||
private const string Camille = "camille";
|
||||
|
||||
public ServiceSouhaitsTests()
|
||||
{
|
||||
_connexion = new SqliteConnection("Data Source=:memory:");
|
||||
_connexion.Open();
|
||||
|
||||
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
|
||||
.UseSqlite(_connexion)
|
||||
.Options;
|
||||
|
||||
_db = new MaBibliDbContext(options);
|
||||
_db.Database.EnsureCreated();
|
||||
|
||||
_souhaits = new ServiceSouhaits(_db);
|
||||
_catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
|
||||
}
|
||||
|
||||
private Task<ResultatSouhait> AjouterAsync(
|
||||
string titre, string? auteur = "Bernard Werber", string utilisateur = Mathieu,
|
||||
string? isbn = null, string? note = null) =>
|
||||
_souhaits.AjouterAsync(
|
||||
new EnregistrementSouhait { Titre = titre, Auteur = auteur, Isbn = isbn, Note = note },
|
||||
utilisateur);
|
||||
|
||||
// ── Portée personnelle ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Deux_utilisateurs_ont_deux_listes_distinctes()
|
||||
{
|
||||
await AjouterAsync("Les fourmis", utilisateur: Mathieu);
|
||||
await AjouterAsync("Germinal", "Émile Zola", Camille);
|
||||
|
||||
var deMathieu = await _souhaits.ListerAsync(Mathieu);
|
||||
var deCamille = await _souhaits.ListerAsync(Camille);
|
||||
|
||||
Assert.Equal("Les fourmis", Assert.Single(deMathieu).Titre);
|
||||
Assert.Equal("Germinal", Assert.Single(deCamille).Titre);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deux_utilisateurs_peuvent_souhaiter_le_meme_livre()
|
||||
{
|
||||
var premier = await AjouterAsync("Les fourmis", utilisateur: Mathieu);
|
||||
var second = await AjouterAsync("Les fourmis", utilisateur: Camille);
|
||||
|
||||
// L'unicité porte sur (utilisateur, œuvre) : elle ne doit pas empêcher deux personnes
|
||||
// d'avoir la même envie — c'est même le cas normal dans un foyer.
|
||||
Assert.Null(premier.Erreur);
|
||||
Assert.Null(second.Erreur);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task On_ne_supprime_pas_l_envie_d_un_autre()
|
||||
{
|
||||
var souhait = await AjouterAsync("Les fourmis", utilisateur: Mathieu);
|
||||
|
||||
var supprime = await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Camille);
|
||||
|
||||
Assert.False(supprime);
|
||||
Assert.Single(await _souhaits.ListerAsync(Mathieu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sans_identite_il_n_y_a_ni_liste_ni_ecriture()
|
||||
{
|
||||
var ajout = await AjouterAsync("Les fourmis", utilisateur: null!);
|
||||
|
||||
Assert.NotNull(ajout.Erreur);
|
||||
Assert.Empty(await _souhaits.ListerAsync(null));
|
||||
}
|
||||
|
||||
// ── Cloisonnement d'avec le catalogue ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// L'invariant qui justifie l'entité séparée : une envie n'entre ni dans le catalogue, ni
|
||||
/// dans les compteurs, ni dans la table des auteurs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Une_envie_n_apparait_pas_dans_le_catalogue()
|
||||
{
|
||||
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
|
||||
|
||||
var catalogue = await _catalogue.ListerAsync(new CritereLivres(), Mathieu);
|
||||
|
||||
Assert.Empty(catalogue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_ne_cree_pas_de_fiche_auteur()
|
||||
{
|
||||
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
|
||||
|
||||
// Sinon « Bernard Werber » apparaîtrait sur l'écran des auteurs avec « 0 livre »,
|
||||
// et trahirait au passage la liste d'envies de son propriétaire.
|
||||
Assert.Empty(await new ServiceAuteurs(_db).ListerAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_recherchee_dans_le_catalogue_ne_remonte_pas()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
|
||||
|
||||
var trouves = await _catalogue.ListerAsync(
|
||||
new CritereLivres { Recherche = "Werber" }, Mathieu);
|
||||
|
||||
Assert.Equal("Les fourmis", Assert.Single(trouves).Titre);
|
||||
}
|
||||
|
||||
// ── Validation et doublons ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Le_titre_est_obligatoire()
|
||||
{
|
||||
var resultat = await AjouterAsync(" ");
|
||||
|
||||
Assert.NotNull(resultat.Erreur);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task La_meme_oeuvre_ne_s_ajoute_pas_deux_fois()
|
||||
{
|
||||
await AjouterAsync("Les thanatonautes");
|
||||
|
||||
// Variante d'écriture du même titre : c'est exactement ce que produit un double clic
|
||||
// sur deux lignes de bibliographie qui n'ont pas été regroupées côté BnF.
|
||||
var doublon = await AjouterAsync("Les thanatonautes : roman");
|
||||
|
||||
Assert.NotNull(doublon.Erreur);
|
||||
Assert.Single(await _souhaits.ListerAsync(Mathieu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deux_auteurs_peuvent_avoir_une_oeuvre_de_meme_titre()
|
||||
{
|
||||
var premier = await AjouterAsync("Nouvelles", "Bernard Werber");
|
||||
var second = await AjouterAsync("Nouvelles", "Émile Zola");
|
||||
|
||||
Assert.Null(premier.Erreur);
|
||||
Assert.Null(second.Erreur);
|
||||
Assert.Equal(2, (await _souhaits.ListerAsync(Mathieu)).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_ISBN_invalide_est_refuse()
|
||||
{
|
||||
var resultat = await AjouterAsync("Les fourmis", isbn: "1234567890123");
|
||||
|
||||
Assert.NotNull(resultat.Erreur);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task L_ISBN_reste_facultatif()
|
||||
{
|
||||
var resultat = await AjouterAsync("Les fourmis", isbn: null);
|
||||
|
||||
Assert.Null(resultat.Erreur);
|
||||
Assert.Null(resultat.Souhait!.Isbn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task La_liste_est_ordonnee_par_auteur_puis_titre()
|
||||
{
|
||||
await AjouterAsync("Les fourmis", "Bernard Werber");
|
||||
await AjouterAsync("Sans auteur", null);
|
||||
await AjouterAsync("Germinal", "Émile Zola");
|
||||
await AjouterAsync("Demain les chats", "Bernard Werber");
|
||||
|
||||
var titres = (await _souhaits.ListerAsync(Mathieu)).Select(s => s.Titre).ToList();
|
||||
|
||||
// Werber avant Zola, ses titres classés entre eux, et l'envie sans auteur en dernier.
|
||||
Assert.Equal(["Demain les chats", "Les fourmis", "Germinal", "Sans auteur"], titres);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_supprimee_disparait()
|
||||
{
|
||||
var souhait = await AjouterAsync("Les fourmis");
|
||||
|
||||
Assert.True(await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Mathieu));
|
||||
Assert.Empty(await _souhaits.ListerAsync(Mathieu));
|
||||
}
|
||||
|
||||
// ── Bibliographie : confrontation à l'étagère ────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Client BnF simulé : ces tests portent sur le rapprochement, pas sur le réseau.
|
||||
/// </summary>
|
||||
private sealed class BnfBibliographieFake(string xml) : IBnfClient
|
||||
{
|
||||
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
|
||||
string isbn, string? urlCouverture, CancellationToken ct = default) =>
|
||||
Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
|
||||
|
||||
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
|
||||
string auteur, CancellationToken ct = default) =>
|
||||
Task.FromResult((BnfBibliographieParser.Parser(xml, auteur), (string?)null));
|
||||
}
|
||||
|
||||
private async Task<BibliographieDto> BibliographieWerberAsync(string utilisateur = Mathieu)
|
||||
{
|
||||
var auteurs = new ServiceAuteurs(_db);
|
||||
var service = new ServiceBibliographie(
|
||||
_db, new BnfBibliographieFake(Fixture.Lire("bnf-auteur-werber.xml")), auteurs);
|
||||
|
||||
var werber = (await auteurs.ListerAsync()).Single(a => a.Nom.Contains("Werber"));
|
||||
return (await service.ObtenirAsync(werber.Id, utilisateur))!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task La_bibliographie_grise_les_livres_deja_possedes()
|
||||
{
|
||||
// Titre saisi sans le sous-titre « : roman » que porte la notice BnF : c'est le cas
|
||||
// courant, et c'est précisément ce que la clé d'œuvre doit rattraper.
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
var biblio = await BibliographieWerberAsync();
|
||||
|
||||
var oeuvre = biblio.Oeuvres.Single(
|
||||
o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Assert.True(oeuvre.Possede);
|
||||
Assert.NotNull(oeuvre.LivreId);
|
||||
|
||||
// Les autres restent à découvrir : posséder un livre n'en grise pas d'autres.
|
||||
Assert.Contains(biblio.Oeuvres, o => o.ADecouvrir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task La_bibliographie_signale_ce_qui_est_deja_souhaite()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
var avant = await BibliographieWerberAsync();
|
||||
var cible = avant.Oeuvres.First(o => o.ADecouvrir);
|
||||
|
||||
await AjouterAsync(cible.Titre, "Bernard Werber");
|
||||
|
||||
var apres = await BibliographieWerberAsync();
|
||||
var revue = apres.Oeuvres.Single(o => o.Titre == cible.Titre);
|
||||
|
||||
Assert.True(revue.Souhaite);
|
||||
Assert.False(revue.ADecouvrir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// « Possédé » vaut pour le foyer, « souhaité » seulement pour l'appelant : c'est ce qui
|
||||
/// permet d'offrir un livre à quelqu'un sans qu'il le voie venir.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Le_souhait_d_un_autre_n_apparait_pas_dans_ma_bibliographie()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
var biblio = await BibliographieWerberAsync(Mathieu);
|
||||
var cible = biblio.Oeuvres.First(o => o.ADecouvrir);
|
||||
|
||||
await AjouterAsync(cible.Titre, "Bernard Werber", Camille);
|
||||
|
||||
var vueDeMathieu = await BibliographieWerberAsync(Mathieu);
|
||||
var vueDeCamille = await BibliographieWerberAsync(Camille);
|
||||
|
||||
Assert.False(vueDeMathieu.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
|
||||
Assert.True(vueDeCamille.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_auteur_inconnu_n_a_pas_de_bibliographie()
|
||||
{
|
||||
var service = new ServiceBibliographie(
|
||||
_db, new BnfBibliographieFake("<x/>"), new ServiceAuteurs(_db));
|
||||
|
||||
Assert.Null(await service.ObtenirAsync(4242, Mathieu));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Le livre d'un homonyme ne doit pas griser l'œuvre d'un autre : la comparaison se fait
|
||||
/// à auteur donné, pas sur tout le catalogue.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Un_livre_d_un_autre_auteur_ne_grise_rien()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
// Un homonyme parfait, mais signé de quelqu'un d'autre.
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Éric Corbeyran" }, Mathieu);
|
||||
|
||||
var biblio = await BibliographieWerberAsync();
|
||||
|
||||
Assert.False(
|
||||
biblio.Oeuvres
|
||||
.Single(o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase))
|
||||
.Possede);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
_connexion.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user