Neue Funde erst prüfen, dann veröffentlichen
Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie freigibt — oder ihnen einen Termin gibt. Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst umgeschaltet; damit stimmt sie nach einem Neustart, nach einer Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan. Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien. Schriebe jede ihren eigenen Filter, reichte eine vergessene — und auffallen würde es nicht beim Programmieren, sondern wenn jemand im Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt, und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die Middleware. Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter umgeht, fällt dort durch. Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und der gesamte Bestand wäre für alle ausser Administratoren verschwunden, sofort beim Start im Container und ohne Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eb63271e92
commit
632d3efa35
@@ -22,6 +22,36 @@ namespace Flaro.Data;
|
||||
/// </remarks>
|
||||
public sealed class FlaroContext(DbContextOptions<FlaroContext> options) : DbContext(options)
|
||||
{
|
||||
/// <summary>Ob diese Anfrage auch Unveröffentlichtes sehen darf.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Hier und nicht in zwanzig Abfragen.</b> Auf Filme und Folgen wird an
|
||||
/// rund zwanzig Stellen in vier Dateien zugegriffen. Schriebe jede ihren
|
||||
/// eigenen Filter, reichte eine vergessene — und auffallen würde es nicht
|
||||
/// beim Programmieren, sondern wenn jemand im Haushalt einen Titel sieht,
|
||||
/// den er nicht sehen sollte. Ein globaler Abfragefilter lässt sich nicht
|
||||
/// vergessen.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Vorgabe <c>true</c>, gesetzt wird je Anfrage.</b> Der Scanner und
|
||||
/// die Metadaten laufen ohne Benutzer und müssen alles sehen — sonst legt
|
||||
/// der nächste Lauf einen wartenden Titel ein zweites Mal an. Für jede
|
||||
/// HTTP-Anfrage setzt die Middleware den Wert, bevor irgendein Endpunkt
|
||||
/// läuft; dass sie das tut, hält ein Test fest.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool AuchUnveroeffentlichtes { get; set; } = true;
|
||||
|
||||
/// <summary>Der Zeitpunkt, gegen den geplante Titel geprüft werden.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Am Kontext und nicht als <c>DateTimeOffset.UtcNow</c> im Filter: ein
|
||||
/// Aufruf mitten im Ausdruck liesse sich nicht nach SQL übersetzen, und
|
||||
/// in Tests wäre die Zeit nicht zu stellen.
|
||||
/// </remarks>
|
||||
public DateTimeOffset Jetzt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<DeviceToken> DeviceTokens => Set<DeviceToken>();
|
||||
public DbSet<AuthCode> AuthCodes => Set<AuthCode>();
|
||||
@@ -238,6 +268,25 @@ public sealed class FlaroContext(DbContextOptions<FlaroContext> options) : DbCon
|
||||
e.HasOne<Series>().WithMany().HasForeignKey(x => x.SeriesId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// **Die Freigaberegel, einmal und für jede Abfrage.**
|
||||
//
|
||||
// Ein Titel ist sichtbar, wenn ein Zeitpunkt gesetzt ist und er
|
||||
// erreicht wurde. Gerechnet statt gespeichert: damit stimmt es nach
|
||||
// einem Neustart, nach einer Zeitumstellung und auch dann, wenn der
|
||||
// Server zum geplanten Zeitpunkt gar nicht lief.
|
||||
b.Entity<Movie>().HasQueryFilter(m =>
|
||||
AuchUnveroeffentlichtes || (m.PublishAt != null && m.PublishAt <= Jetzt));
|
||||
|
||||
b.Entity<Episode>().HasQueryFilter(f =>
|
||||
AuchUnveroeffentlichtes || (f.PublishAt != null && f.PublishAt <= Jetzt));
|
||||
|
||||
// **Die Serie hängt an ihren Folgen.** Ein eigenes Feld gäbe zwei
|
||||
// Wahrheiten — eine veröffentlichte Serie, deren Folgen alle noch
|
||||
// warten, wäre ein leerer Eintrag in der Mediathek.
|
||||
b.Entity<Series>().HasQueryFilter(s =>
|
||||
AuchUnveroeffentlichtes
|
||||
|| Episodes.Any(f => f.SeriesId == s.Id && f.PublishAt != null && f.PublishAt <= Jetzt));
|
||||
|
||||
b.Entity<ItemTranslation>(e =>
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
|
||||
Reference in New Issue
Block a user