Phase 3 : CRUD des livres, statuts de lecture et identite SSOwat
API - CRUD complet sur /api/livres : liste filtrable (format, statut) et cherchable (titre, auteur), consultation, creation, modification, changement de statut isole, suppression. - Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne puisse pas s'en servir par inadvertance. Deux tests verrouillent l'invariant, dont un par reflexion sur le type des criteres. - AjoutePar est renseigne par le serveur a la creation, jamais par le client, et n'est pas reecrit a l'edition (pas plus que DateAjout). - Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL, YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en developpement uniquement. Sans en-tete ni simulation, l'identite reste inconnue plutot qu'inventee. Expose par GET /api/moi. - Migration appliquee au demarrage : pas d'etape manuelle a l'installation. Interface Blazor, pensee mobile d'abord - Catalogue unique (physiques et numeriques melanges, distingues par le format) avec recherche, filtres et changement de statut directement dans la liste. - Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de choix les presente en mettant en avant editeur et annee, seuls elements qui les departagent. Le formulaire reste ensuite entierement modifiable. - Ajout manuel, edition, suppression avec confirmation explicite. - Couvertures : l'URL OpenLibrary n'est jamais validee par un appel prealable (502 intermittents mesures en phase 2) ; un attribut onerror masque l'image cassee et laisse apparaitre un substitut. Decisions prises la ou CLAUDE.md etait muet - Tri alphabetique par titre. - Un ISBN saisi doit etre valide, mais reste facultatif. - Ecran de choix affiche seulement a partir de deux candidats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using MaBibli.Shared.Entites;
|
||||
|
||||
namespace MaBibli.Shared.Catalogue;
|
||||
|
||||
/// <summary>
|
||||
/// Critères de consultation du catalogue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Il n'existe volontairement aucun critère sur <c>AjoutePar</c>.</b> La bibliothèque est
|
||||
/// commune à tout le foyer : filtrer les lectures sur ce champ serait un bug de conception.
|
||||
/// L'absence de ce critère est l'endroit où l'invariant est le plus visible.
|
||||
/// </remarks>
|
||||
public record CritereLivres
|
||||
{
|
||||
/// <summary>Texte cherché dans le titre <i>ou</i> l'auteur. Insensible à la casse.</summary>
|
||||
public string? Recherche { get; init; }
|
||||
|
||||
/// <summary>Physique ou numérique. <c>null</c> = les deux : le catalogue est unique.</summary>
|
||||
public Format? Format { get; init; }
|
||||
|
||||
/// <summary>Statut de lecture. <c>null</c> = tous.</summary>
|
||||
public Statut? Statut { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MaBibli.Shared.Entites;
|
||||
|
||||
namespace MaBibli.Shared.Catalogue;
|
||||
|
||||
/// <summary>
|
||||
/// Filtrage, recherche et tri du catalogue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Écrit sur <see cref="IQueryable{T}"/> pour être exécuté par EF Core côté base <b>et</b>
|
||||
/// testable en mémoire sur une simple liste, sans base ni réseau.
|
||||
/// <para>
|
||||
/// La comparaison passe par <c>ToLower()</c> : EF Core la traduit en SQL, et LINQ-to-Objects
|
||||
/// lui donne le même comportement insensible à la casse. Les accents ne sont pas normalisés
|
||||
/// (« Emile » ne trouve pas « Émile ») — limitation assumée à ce stade.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class FiltreLivres
|
||||
{
|
||||
public static IQueryable<Livre> Appliquer(IQueryable<Livre> source, CritereLivres criteres)
|
||||
{
|
||||
// AUCUN filtre sur AjoutePar : la bibliothèque est commune (CLAUDE.md).
|
||||
// Ce commentaire est là pour qu'une régression future saute aux yeux en relecture.
|
||||
|
||||
if (criteres.Format is { } format)
|
||||
{
|
||||
source = source.Where(l => l.Format == format);
|
||||
}
|
||||
|
||||
if (criteres.Statut is { } statut)
|
||||
{
|
||||
source = source.Where(l => l.Statut == statut);
|
||||
}
|
||||
|
||||
var terme = criteres.Recherche?.Trim();
|
||||
if (!string.IsNullOrEmpty(terme))
|
||||
{
|
||||
var recherche = terme.ToLower();
|
||||
source = source.Where(l =>
|
||||
l.Titre.ToLower().Contains(recherche)
|
||||
|| (l.Auteur != null && l.Auteur.ToLower().Contains(recherche)));
|
||||
}
|
||||
|
||||
return Trier(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tri par titre, puis par identifiant pour rendre l'ordre déterministe.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// CLAUDE.md ne tranche pas le tri. L'ordre alphabétique a été retenu parce qu'une
|
||||
/// bibliothèque se parcourt par titre ; l'ordre d'ajout n'a d'intérêt que le jour de la saisie.
|
||||
/// </remarks>
|
||||
public static IQueryable<Livre> Trier(IQueryable<Livre> source) =>
|
||||
source.OrderBy(l => l.Titre).ThenBy(l => l.Id);
|
||||
}
|
||||
Reference in New Issue
Block a user