Bug 1 (500 on watermark/graphics save): Replace/Add handlers projected the freshly-built entity graph, whose ProgramScheduleItemWatermark / -GraphicsElement join rows carry only foreign-key ids — the Watermark/GraphicsElement navs are null, and Mapper.ProjectToViewModel dereferences them unguarded, throwing an NRE that the controller surfaced as a 500 on PUT/POST. Both handlers now reload the persisted item(s) through the read-side include chain before projecting. Extracted that chain into ProgramScheduleItemQueryExtensions.IncludeScheduleItemDetails() so GET, Replace and Add share one source of truth. Masking: PersistItems returned a lazy LanguageExt Map, and the existing round-trip test only checked .IsRight — never enumerating it, so the deferred NRE never fired. The new ScheduleItemWriteProjectionTests force enumeration (as the controller's .ToList()/serialization does) and seed watermark/graphics via a separate context so the handler's fresh factory context has nothing pre-tracked. Bug 2 (server side): GetProgramScheduleItemsHandler now .OrderBy(i => i.Index) — it previously returned id order, which is not index order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
61 lines
2.3 KiB
C#
61 lines
2.3 KiB
C#
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Infrastructure.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using static ErsatzTV.Application.ProgramSchedules.Mapper;
|
|
|
|
namespace ErsatzTV.Application.ProgramSchedules;
|
|
|
|
public class GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbContextFactory) :
|
|
IRequestHandler<GetProgramScheduleItems, List<ProgramScheduleItemViewModel>>
|
|
{
|
|
public async Task<List<ProgramScheduleItemViewModel>> Handle(
|
|
GetProgramScheduleItems request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
|
|
Option<ProgramSchedule> maybeProgramSchedule =
|
|
await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id, cancellationToken);
|
|
|
|
return await dbContext.ProgramScheduleItems
|
|
.Filter(psi => psi.ProgramScheduleId == request.Id)
|
|
.IncludeScheduleItemDetails()
|
|
.OrderBy(i => i.Index)
|
|
.ToListAsync(cancellationToken)
|
|
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
|
|
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());
|
|
}
|
|
|
|
// shuffled schedule items supports a limited set of property values
|
|
private static ProgramScheduleItemViewModel EnforceProperties(
|
|
Option<ProgramSchedule> maybeProgramSchedule,
|
|
ProgramScheduleItemViewModel item)
|
|
{
|
|
foreach (ProgramSchedule programSchedule in maybeProgramSchedule)
|
|
{
|
|
if (programSchedule.ShuffleScheduleItems)
|
|
{
|
|
item = item with { StartType = StartType.Dynamic };
|
|
if (item.PlayoutMode == PlayoutMode.Flood)
|
|
{
|
|
item = item with { PlayoutMode = PlayoutMode.One };
|
|
}
|
|
}
|
|
|
|
if (item.PlaybackOrder is PlaybackOrder.ShuffleInOrder)
|
|
{
|
|
item = item with { FillWithGroupMode = FillWithGroupMode.None };
|
|
}
|
|
|
|
if (item.CollectionType is CollectionType.Playlist or CollectionType.RerunFirstRun
|
|
or CollectionType.RerunRerun)
|
|
{
|
|
item = item with { PlaybackOrder = PlaybackOrder.None };
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
}
|