Sortir le statut de lecture et l'auteur de la table Livre
Deux changements de modele en une seule migration, plus la recherche qui en depend. Le statut de lecture devient personnel. Il etait une colonne de Livre, donc partage par tout le foyer, alors que deux membres lisent le meme exemplaire a des rythmes differents. Il vit desormais dans une table (LivreId, Utilisateur, Statut) avec unicite sur le couple. L'absence de ligne vaut « non commence » : sur une bibliotheque de foyer la plupart des couples n'ont aucun statut, et les materialiser tous multiplierait les lignes par le nombre de comptes pour n'exprimer qu'un vide. Rien n'est donc ecrit a la creation d'un livre. Piege rencontre : un Dictionary<int, Statut> renvoyait la valeur 0 de l'enumeration — « À lire » — pour un livre sans ligne, rendant « non commence » indiscernable d'un choix explicite. Le dictionnaire est desormais typé Statut?. L'auteur devient une table. Deux formes normalisees y cohabitent, et ce n'est pas une redondance : NomNormalise garde l'ordre de saisie pour la recherche en sous-chaine, CleRegroupement trie les mots et porte l'index unique, donc l'invariant « un auteur, une fiche ». Les initiales echappent a la cle et sont traitees en memoire, sur une table qui compte au plus quelques centaines de lignes. Un livre peut avoir plusieurs auteurs — le lookup ISBN en renvoie quatre pour Introduction to Algorithms — d'ou la table de liaison, avec une position qui conserve l'ordre de la couverture. Les rapprochements ambigus ne sont jamais appliques seuls : l'API les liste, l'utilisateur accepte ou refuse, et les refus sont memorises pour que la suggestion ne revienne pas. Le couple refuse est range par identifiant croissant, donc un refus vaut dans les deux sens. Reprise des donnees existantes. L'ancien statut, commun, est rattache a AjoutePar — seule personne que la base associe au livre. Les statuts des livres sans AjoutePar sont perdus : les attribuer serait une invention. Les trois valeurs sont reprises telles quelles, « À lire » compris, parce que c'est ce que l'ancienne interface affichait. L'ancien champ auteur devient une fiche par valeur distincte. La migration ne peut pas tout faire : lower() de SQLite ne retire pas les accents, donc « Émile Zola » et « emile zola » y restent deux fiches. ServiceRenormalisation finit le travail en C# au demarrage, reunit ces variantes, applique aussi la regle des initiales — sans quoi une base heritee resterait eclatee la ou une saisie neuve aurait ete reunie d'emblee — et garde le nom d'affichage le plus presentable. Il est idempotent, et sert de filet si les regles de normalisation changent. L'ordre de la migration compte : les colonnes condamnees sont recopiees dans une table de transit avant d'etre supprimees, parce que supprimer une colonne sous SQLite reconstruit la table. Verifie sur une base a l'ancien schema contenant 9 livres, 2 prets et trois variantes de Zola : prets intacts, statuts rattaches, les trois Zola reunis sous « Émile Zola », « P.F. Hamilton » absorbe par « Peter F. Hamilton », « Hamilton » seul laisse en suggestion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,24 @@ public record CandidatLivre
|
||||
/// <summary>Titre nettoyé de la ponctuation ISBD.</summary>
|
||||
public required string Titre { get; init; }
|
||||
|
||||
/// <summary>Auteur au format « Prénom Nom », ou <c>null</c> si la source n'en donne pas.</summary>
|
||||
public string? Auteur { get; init; }
|
||||
/// <summary>
|
||||
/// Auteurs au format « Prénom Nom », dans l'ordre donné par la source. Peut être vide.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Une liste, et non une chaîne : <i>Introduction to Algorithms</i> en renvoie quatre
|
||||
/// (Cormen, Leiserson, Rivest, Stein), et les recoller pour les redécouper ensuite ferait
|
||||
/// perdre la frontière entre « Zola, Émile » et deux auteurs.
|
||||
/// </remarks>
|
||||
public IReadOnlyList<string> Auteurs { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Les auteurs sur une ligne, séparés comme les attend un champ de saisie
|
||||
/// (<see cref="EnregistrementLivre.SeparateurAuteurs"/>).
|
||||
/// </summary>
|
||||
public string? Auteur =>
|
||||
Auteurs.Count == 0
|
||||
? null
|
||||
: string.Join($"{EnregistrementLivre.SeparateurAuteurs} ", Auteurs);
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MaBibli.Shared.Entites;
|
||||
|
||||
namespace MaBibli.Shared.Dtos;
|
||||
@@ -11,24 +12,67 @@ namespace MaBibli.Shared.Dtos;
|
||||
/// </remarks>
|
||||
public record EnregistrementLivre
|
||||
{
|
||||
/// <summary>Séparateur des auteurs dans la forme texte d'un champ de saisie.</summary>
|
||||
/// <remarks>
|
||||
/// Le point-virgule, et non la virgule : celle-ci est déjà prise par la forme
|
||||
/// bibliographique « Zola, Émile », qu'on doit pouvoir taper telle quelle.
|
||||
/// </remarks>
|
||||
public const char SeparateurAuteurs = ';';
|
||||
|
||||
/// <summary>Facultatif : certains livres n'ont pas d'ISBN. Normalisé par le serveur.</summary>
|
||||
public string? Isbn { get; set; }
|
||||
|
||||
public string Titre { get; set; } = string.Empty;
|
||||
|
||||
public string? Auteur { get; set; }
|
||||
/// <summary>
|
||||
/// Noms des auteurs, dans l'ordre de la couverture. Le serveur les rattache aux fiches
|
||||
/// existantes quand c'est sûr, et en crée sinon.
|
||||
/// </summary>
|
||||
public List<string> Auteurs { get; set; } = [];
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
|
||||
public Format Format { get; set; }
|
||||
|
||||
public Statut Statut { get; set; }
|
||||
/// <summary>
|
||||
/// Statut de lecture <b>de l'appelant</b>. <c>null</c> = non commencé, et aucune ligne n'est
|
||||
/// alors écrite en base.
|
||||
/// </summary>
|
||||
public Statut? Statut { get; set; }
|
||||
|
||||
public string? CoverUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Vue « une seule ligne » de <see cref="Auteurs"/>, pour les champs de saisie simples.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Non sérialisée : <see cref="Auteurs"/> est la forme de référence sur le fil. Cette
|
||||
/// propriété existe pour que l'interface puisse lier un <c><input></c> unique sans
|
||||
/// avoir à gérer une liste dynamique — un foyer saisit un auteur dans l'immense majorité
|
||||
/// des cas, et les rares livres à quatre auteurs se tapent avec des « ; ».
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string? Auteur
|
||||
{
|
||||
get => Auteurs.Count == 0 ? null : string.Join($"{SeparateurAuteurs} ", Auteurs);
|
||||
set => Auteurs = Decouper(value);
|
||||
}
|
||||
|
||||
/// <summary>Découpe une saisie texte en noms d'auteurs, en écartant les entrées vides.</summary>
|
||||
public static List<string> Decouper(string? saisie) =>
|
||||
string.IsNullOrWhiteSpace(saisie)
|
||||
? []
|
||||
: saisie.Split(SeparateurAuteurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Charge utile du changement de statut de lecture — l'action la plus fréquente.</summary>
|
||||
/// <remarks>
|
||||
/// Le statut posé est toujours celui de l'appelant : il n'y a pas de champ « utilisateur »,
|
||||
/// et il ne doit jamais y en avoir. Personne ne coche « lu » à la place d'un autre.
|
||||
/// </remarks>
|
||||
public record ChangementStatut
|
||||
{
|
||||
public Statut Statut { get; set; }
|
||||
/// <summary><c>null</c> retire le statut : le livre redevient « non commencé » pour l'appelant.</summary>
|
||||
public Statut? Statut { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MaBibli.Shared.Entites;
|
||||
|
||||
namespace MaBibli.Shared.Dtos;
|
||||
|
||||
/// <summary>Un auteur tel qu'exposé par l'API : de quoi l'afficher et cliquer dessus.</summary>
|
||||
public record AuteurDto
|
||||
{
|
||||
public required int Id { get; init; }
|
||||
|
||||
public required string Nom { get; init; }
|
||||
|
||||
/// <summary>Nombre de livres du catalogue rattachés à cet auteur. Renseigné par la liste des auteurs.</summary>
|
||||
public int NombreDeLivres { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Un livre tel qu'exposé par l'API. Volontairement sans la collection <c>Prets</c> :
|
||||
/// les prêts sont la phase 4.
|
||||
@@ -14,13 +26,22 @@ public record LivreDto
|
||||
|
||||
public required string Titre { get; init; }
|
||||
|
||||
public string? Auteur { get; init; }
|
||||
/// <summary>Auteurs, dans l'ordre de la couverture. Peut être vide.</summary>
|
||||
public IReadOnlyList<AuteurDto> Auteurs { get; init; } = [];
|
||||
|
||||
public string? Editeur { get; init; }
|
||||
|
||||
public required Format Format { get; init; }
|
||||
|
||||
public required Statut Statut { get; init; }
|
||||
/// <summary>
|
||||
/// Statut de lecture <b>de l'utilisateur courant</b>, ou <c>null</c> s'il n'en a jamais posé
|
||||
/// (« non commencé »).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ce champ dépend de qui appelle : le même livre remonte « Lu » pour l'un et <c>null</c>
|
||||
/// pour l'autre. C'est voulu — le livre est commun, sa lecture ne l'est pas.
|
||||
/// </remarks>
|
||||
public Statut? Statut { get; init; }
|
||||
|
||||
public string? CoverUrl { get; init; }
|
||||
|
||||
@@ -31,4 +52,9 @@ public record LivreDto
|
||||
/// la bibliothèque est commune et aucune lecture ne filtre sur ce champ.
|
||||
/// </summary>
|
||||
public string? AjoutePar { get; init; }
|
||||
|
||||
/// <summary>Auteurs sur une ligne, pour un affichage compact.</summary>
|
||||
[JsonIgnore]
|
||||
public string? AuteursAffiches =>
|
||||
Auteurs.Count == 0 ? null : string.Join(", ", Auteurs.Select(a => a.Nom));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace MaBibli.Shared.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Un rapprochement d'auteurs <b>proposé</b> à l'utilisateur, jamais appliqué d'office.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Les variantes sûres (casse, accents, ordre, initiales) sont fusionnées automatiquement à
|
||||
/// l'écriture et n'apparaissent donc jamais ici. Ne restent que les cas ambigus, typiquement un
|
||||
/// patronyme seul face à un nom complet : « Hamilton » et « Peter F. Hamilton » peuvent être la
|
||||
/// même personne ou deux membres d'une même famille, et seule l'utilisatrice le sait.
|
||||
/// </remarks>
|
||||
public record SuggestionRapprochement
|
||||
{
|
||||
/// <summary>Auteur au nom le plus court — celui qui disparaîtrait en cas de fusion.</summary>
|
||||
public required AuteurDto Source { get; init; }
|
||||
|
||||
/// <summary>Auteur au nom le plus complet — celui qui absorberait l'autre.</summary>
|
||||
public required AuteurDto Cible { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Décision de l'utilisateur sur un rapprochement proposé.</summary>
|
||||
public record DecisionRapprochement
|
||||
{
|
||||
public required int SourceId { get; set; }
|
||||
|
||||
public required int CibleId { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user