Enhance GridPlanner and GenerateGridDialog for improved slot management and user experience
Refactored the GridPlanner class to optimize slot management during grid generation, ensuring more efficient handling of slots. Updated the GenerateGridDialog component to improve its layout and usability, enhancing the overall user experience when interacting with slot lists. These changes contribute to better functionality and accessibility in grid management.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Groups.FindGroupCandidates;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Подбор позиций по правилу набора: то, что редактор группы показывает кнопкой «Подобрать».
|
||||
/// Важна не только выборка, но и отметка «уже в группе» — по ней видно, что добавлять нечего.
|
||||
/// </summary>
|
||||
public class FindGroupCandidatesTests
|
||||
{
|
||||
private static (Show Show, MediaAsset Asset) Playable(string name, int minutes)
|
||||
{
|
||||
var asset = MediaAsset.Register($"{name}.mkv", ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkReady(
|
||||
new MediaReadyInfo(
|
||||
TimeSpan.FromMinutes(minutes),
|
||||
6,
|
||||
minutes * 10,
|
||||
1920,
|
||||
1080,
|
||||
"h264",
|
||||
"aac",
|
||||
$"assets/{name}"
|
||||
)
|
||||
);
|
||||
var show = Show.Create(name, ShowKind.Single);
|
||||
show.AddEpisode(asset.Id);
|
||||
return (show, asset);
|
||||
}
|
||||
|
||||
private static async Task<TeleWave.Application.Common.Models.Result<
|
||||
IReadOnlyList<GroupCandidateDto>
|
||||
>> FindAsync(TestDb fixture, FindGroupCandidatesQuery query)
|
||||
{
|
||||
await using var db = fixture.New();
|
||||
return await new FindGroupCandidatesQueryHandler(
|
||||
db,
|
||||
new GroupFilterMatcher(db),
|
||||
new GroupElementResolver(db)
|
||||
).Handle(query, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarksWhatIsAlreadyInGroup_AndFiltersByUnitDuration()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var shortFilm = Playable("Короткий", 20);
|
||||
var longFilm = Playable("Длинный", 120);
|
||||
var inside = Playable("Уже внутри", 100);
|
||||
|
||||
var group = Group.Create("Кино");
|
||||
group.SetFilter(new GroupFilter(UnitMinutesMin: 60).ToJson());
|
||||
group.AddElement(GroupElementKind.Show, inside.Show.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.AddRange(shortFilm.Asset, longFilm.Asset, inside.Asset);
|
||||
seed.Shows.AddRange(shortFilm.Show, longFilm.Show, inside.Show);
|
||||
seed.Groups.Add(group);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var result = await FindAsync(fixture, new FindGroupCandidatesQuery(group.Id));
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// Короткий отсечён по длине единицы: правило меряет саму серию, а не сумму по шоу.
|
||||
Assert.Equal(["Длинный", "Уже внутри"], result.Value.Select(c => c.ElementName));
|
||||
Assert.False(result.Value.Single(c => c.ElementName == "Длинный").AlreadyInGroup);
|
||||
Assert.True(result.Value.Single(c => c.ElementName == "Уже внутри").AlreadyInGroup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesFilterFromQuery_WhenGivenExplicitly()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var film = Playable("Кино", 100);
|
||||
// У группы своего правила нет — редактор крутит фильтр до сохранения.
|
||||
var group = Group.Create("Черновик");
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(film.Asset);
|
||||
seed.Shows.Add(film.Show);
|
||||
seed.Groups.Add(group);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var withFilter = await FindAsync(
|
||||
fixture,
|
||||
new FindGroupCandidatesQuery(group.Id, new GroupFilter(ShowKinds: [ShowKind.Single]))
|
||||
);
|
||||
var withoutFilter = await FindAsync(fixture, new FindGroupCandidatesQuery(group.Id));
|
||||
|
||||
Assert.Single(withFilter.Value);
|
||||
// Без правила подбирать не из чего — и это отказ, а не пустой список.
|
||||
Assert.False(withoutFilter.IsSuccess);
|
||||
Assert.Equal(GroupErrors.FilterNotSet, withoutFilter.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fails_WhenGroupIsGone()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
|
||||
var result = await FindAsync(fixture, new FindGroupCandidatesQuery(Guid.NewGuid()));
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal(GroupErrors.NotFound, result.Error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.GetTemplate;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Сетка глазами редактора: слои по приоритету, слоты с именами групп и признаки состояния —
|
||||
/// есть ли неприменённые правки и есть ли к чему откатываться.
|
||||
/// </summary>
|
||||
public class GetTemplateTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsLayersByPriority_WithGroupNamesAndState()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var group = Group.Create("Кино");
|
||||
var channel = Channel.Create("Первый", "first", T0);
|
||||
channel.UpdateTimeSettings(1, 180, new TimeOnly(6, 0));
|
||||
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
channel.SetTemplate(template.Id);
|
||||
var top = template.AddLayer("Выходные", 20);
|
||||
var slot = top.AddSlot("Кино в прайм", new TimeOnly(20, 0), 120, Daypart.Prime, weekday: 6);
|
||||
slot.UpdateContent(
|
||||
new SlotContent(
|
||||
slot.Title,
|
||||
SlotKind.Content,
|
||||
group.Id,
|
||||
new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14).ToJson(),
|
||||
null,
|
||||
SlotBlockMode.Count,
|
||||
1,
|
||||
OverflowPolicy.ContinueNext
|
||||
)
|
||||
);
|
||||
template.MarkChanged();
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Groups.Add(group);
|
||||
seed.Channels.Add(channel);
|
||||
seed.ScheduleTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelTemplateQueryHandler(db).Handle(
|
||||
new GetChannelTemplateQuery(channel.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
var dto = result.Value;
|
||||
|
||||
// Слои идут сверху вниз: первым тот, что перекрывает остальные.
|
||||
Assert.Equal(["Выходные", "Фон"], dto.Layers.Select(l => l.Name));
|
||||
Assert.Equal(new TimeOnly(6, 0), dto.DayStartTime);
|
||||
Assert.Equal(180, dto.UtcOffsetMinutes);
|
||||
|
||||
var stored = Assert.Single(dto.Layers[0].Slots);
|
||||
// Имя группы приезжает вместе со слотом: инспектор показывает его без второго запроса.
|
||||
Assert.Equal("Кино", stored.GroupName);
|
||||
Assert.Equal(6, stored.Weekday);
|
||||
Assert.Equal(SlotStrategyType.RandomWithCooldown, stored.Strategy!.Type);
|
||||
Assert.Equal(14, stored.Strategy.CooldownDays);
|
||||
|
||||
// Правка эфир не двигает — она поднимает ревизию, и плашка об этом видна в шапке канала.
|
||||
Assert.True(dto.HasPendingChanges);
|
||||
Assert.Equal(1, dto.Revision);
|
||||
Assert.Equal(0, dto.AppliedRevision);
|
||||
// Сетку ни разу не применяли — откатывать не к чему, и кнопки быть не должно.
|
||||
Assert.False(dto.CanRestore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRestore_AfterFirstApply()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "first", T0);
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
channel.SetTemplate(template.Id);
|
||||
template.MarkApplied(TemplateSnapshot.Capture(template).ToJson());
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.ScheduleTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelTemplateQueryHandler(db).Handle(
|
||||
new GetChannelTemplateQuery(channel.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.Value.CanRestore);
|
||||
Assert.False(result.Value.HasPendingChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fails_WhenChannelOrTemplateIsMissing()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Без сетки", "no-grid", T0);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new GetChannelTemplateQueryHandler(db);
|
||||
|
||||
var noChannel = await handler.Handle(
|
||||
new GetChannelTemplateQuery(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
var noTemplate = await handler.Handle(
|
||||
new GetChannelTemplateQuery(channel.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.False(noChannel.IsSuccess);
|
||||
Assert.False(noTemplate.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ElementResolver_ExpandsCollectionIntoItsParts()
|
||||
{
|
||||
// Коллекция в группе — одна позиция, но эфира она даёт столько же, сколько все её части.
|
||||
var fixture = new TestDb();
|
||||
var film = Show.Create("Часть 1", ShowKind.Single);
|
||||
film.SetAudience(ShowAudience.Pg13);
|
||||
var sequel = Show.Create("Часть 2", ShowKind.Single);
|
||||
sequel.SetAudience(ShowAudience.R);
|
||||
var assets = new List<MediaAsset>();
|
||||
|
||||
foreach (var show in new[] { film, sequel })
|
||||
{
|
||||
var asset = MediaAsset.Register($"{show.Name}.mkv", ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkReady(
|
||||
new MediaReadyInfo(
|
||||
TimeSpan.FromMinutes(90),
|
||||
6,
|
||||
900,
|
||||
1920,
|
||||
1080,
|
||||
"h264",
|
||||
"aac",
|
||||
$"assets/{show.Name}"
|
||||
)
|
||||
);
|
||||
assets.Add(asset);
|
||||
show.AddEpisode(asset.Id);
|
||||
}
|
||||
|
||||
var collection = Collection.Create("Франшиза");
|
||||
collection.AddShow(film.Id);
|
||||
collection.AddShow(sequel.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.AddRange(assets);
|
||||
seed.Shows.AddRange(film, sequel);
|
||||
seed.Collections.Add(collection);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var info = await new GroupElementResolver(db).ResolveAsync(
|
||||
[(GroupElementKind.Collection, collection.Id), (GroupElementKind.Show, film.Id)],
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
var franchise = info[(GroupElementKind.Collection, collection.Id)];
|
||||
Assert.Equal("Франшиза", franchise.Name);
|
||||
Assert.Equal(2, franchise.UnitCount);
|
||||
Assert.Equal(TimeSpan.FromMinutes(180), franchise.TotalDuration);
|
||||
// Рейтинг франшизы — самый строгий среди частей: по нему её отсеивают в детское время.
|
||||
Assert.Equal(ShowAudience.R, franchise.Audience);
|
||||
|
||||
var single = info[(GroupElementKind.Show, film.Id)];
|
||||
Assert.Equal(1, single.UnitCount);
|
||||
Assert.Equal(ShowKind.Single, single.ShowKind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.Generate;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Витрина автосборки: каталог профилей, предпросмотр плана и его перевод в DTO. Предпросмотр —
|
||||
/// то, по чему админ принимает решение, поэтому проверяется отдельно от самой раскладки.
|
||||
/// </summary>
|
||||
public class GridPlanPreviewTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static PlannedSlot Slot(
|
||||
string title,
|
||||
SlotKind kind,
|
||||
SlotBlockMode blockMode,
|
||||
int blockValue,
|
||||
int? weekday = null
|
||||
) =>
|
||||
new(
|
||||
GridPlanLayer.Main,
|
||||
weekday,
|
||||
new TimeOnly(20, 0),
|
||||
120,
|
||||
title,
|
||||
Daypart.Prime,
|
||||
kind,
|
||||
kind == SlotKind.Content ? Guid.NewGuid() : null,
|
||||
kind == SlotKind.Content ? title : null,
|
||||
blockMode,
|
||||
blockValue,
|
||||
null,
|
||||
null,
|
||||
IsAnchor: false
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Profiles_AreListed_WithReferenceAndDescription()
|
||||
{
|
||||
var result = await new ListGridProfilesQueryHandler().Handle(
|
||||
new ListGridProfilesQuery(),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// Каждый профиль должен объяснять себя: имя, на что похоже и для какой библиотеки годится.
|
||||
Assert.Equal(GridProfiles.All.Count, result.Value.Count);
|
||||
Assert.All(
|
||||
result.Value,
|
||||
profile =>
|
||||
{
|
||||
Assert.False(string.IsNullOrWhiteSpace(profile.Name));
|
||||
Assert.False(string.IsNullOrWhiteSpace(profile.Reference));
|
||||
Assert.False(string.IsNullOrWhiteSpace(profile.Description));
|
||||
}
|
||||
);
|
||||
Assert.Contains(result.Value, p => p.Kind == GridProfileKind.Animation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mapper_DescribesEveryBlockKind()
|
||||
{
|
||||
var plan = new GridPlan(
|
||||
GridProfiles.Get(GridProfileKind.Mixed),
|
||||
GridGenerationMode.Rebuild,
|
||||
[
|
||||
Slot("Сериалы", SlotKind.Content, SlotBlockMode.Count, 3),
|
||||
Slot("Кино", SlotKind.Content, SlotBlockMode.FillSlot, 1),
|
||||
Slot("Полоса", SlotKind.Content, SlotBlockMode.Duration, 90, weekday: 6),
|
||||
Slot("Повтор", SlotKind.Repeat, SlotBlockMode.FillSlot, 1),
|
||||
Slot("Конец вещания", SlotKind.SignOff, SlotBlockMode.FillSlot, 1),
|
||||
],
|
||||
["замечание"],
|
||||
SlotsToRemove: 4,
|
||||
FallbackGroupId: Guid.NewGuid(),
|
||||
FallbackGroupName: "Фон",
|
||||
FreeMinutes: 10080,
|
||||
CoveredMinutes: 5040
|
||||
);
|
||||
|
||||
var dto = plan.ToDto();
|
||||
|
||||
Assert.Equal(GridProfileKind.Mixed, dto.Profile);
|
||||
Assert.Equal(plan.Profile.Name, dto.ProfileName);
|
||||
Assert.Equal(4, dto.SlotsToRemove);
|
||||
Assert.Equal("Фон", dto.FallbackGroupName);
|
||||
Assert.Equal(["замечание"], dto.Notes);
|
||||
|
||||
// Описание блока — то, что читает админ вместо режима и числа.
|
||||
Assert.Equal(
|
||||
["3 подряд", "до конца слота", "90 мин", "повтор", "конец вещания"],
|
||||
dto.Slots.Select(s => s.Block)
|
||||
);
|
||||
// Время приезжает строкой «ЧЧ:ММ»: в предпросмотре оно только показывается.
|
||||
Assert.All(dto.Slots, slot => Assert.Equal("20:00", slot.Start));
|
||||
Assert.Equal(6, dto.Slots[2].Weekday);
|
||||
Assert.Null(dto.Slots[0].Weekday);
|
||||
// У неконтентных слотов группы нет — показывать в колонке нечего.
|
||||
Assert.Null(dto.Slots[3].GroupName);
|
||||
Assert.Equal("Сериалы", dto.Slots[0].GroupName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Preview_ReturnsPlanForProfile()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new PreviewGeneratedGridQueryHandler(
|
||||
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
).Handle(
|
||||
new PreviewGeneratedGridQuery(
|
||||
channelId,
|
||||
GridProfileKind.Kids,
|
||||
GridGenerationMode.Rebuild
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(GridProfileKind.Kids, result.Value.Profile);
|
||||
Assert.NotEmpty(result.Value.Slots);
|
||||
// Пересборка сносит то, что было, и это видно до нажатия.
|
||||
Assert.True(result.Value.SlotsToRemove >= 0);
|
||||
Assert.Equal(7 * GridCoverage.MinutesInDay, result.Value.FreeMinutes);
|
||||
// У детского профиля ночь — конец вещания, и в предпросмотре он подписан именно так.
|
||||
Assert.Contains(result.Value.Slots, s => s.Block == "конец вещания");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Preview_FailsWhenChannelIsGone()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var result = await new PreviewGeneratedGridQueryHandler(
|
||||
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
).Handle(
|
||||
new PreviewGeneratedGridQuery(
|
||||
Guid.NewGuid(),
|
||||
GridProfileKind.Mixed,
|
||||
GridGenerationMode.FillGaps
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_RejectsUnknownProfileAndMode()
|
||||
{
|
||||
var validator = new GenerateGridCommandValidator();
|
||||
|
||||
Assert.True(
|
||||
validator
|
||||
.Validate(
|
||||
new GenerateGridCommand(
|
||||
Guid.NewGuid(),
|
||||
GridProfileKind.Sitcom,
|
||||
GridGenerationMode.Rebuild
|
||||
)
|
||||
)
|
||||
.IsValid
|
||||
);
|
||||
// Значения enum приходят с клиента строкой — за пределами каталога они бессмысленны.
|
||||
Assert.False(
|
||||
validator
|
||||
.Validate(
|
||||
new GenerateGridCommand(
|
||||
Guid.NewGuid(),
|
||||
(GridProfileKind)42,
|
||||
GridGenerationMode.FillGaps
|
||||
)
|
||||
)
|
||||
.IsValid
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Канал с одной группой мягкого рейтинга — чтобы детскому профилю было чем закрыться.</summary>
|
||||
private static async Task<Guid> SeedAsync(TestDb fixture)
|
||||
{
|
||||
var show = Show.Create("Мультсериал", ShowKind.Series);
|
||||
show.SetAudience(ShowAudience.G);
|
||||
var assets = new List<MediaAsset>();
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var asset = MediaAsset.Register($"e{i}.mkv", ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkReady(
|
||||
new MediaReadyInfo(
|
||||
TimeSpan.FromMinutes(22),
|
||||
6,
|
||||
220,
|
||||
1920,
|
||||
1080,
|
||||
"h264",
|
||||
"aac",
|
||||
$"assets/e{i}"
|
||||
)
|
||||
);
|
||||
assets.Add(asset);
|
||||
show.AddEpisode(asset.Id);
|
||||
}
|
||||
|
||||
var group = Group.Create("Мультфильмы");
|
||||
group.AddElement(GroupElementKind.Show, show.Id);
|
||||
|
||||
var channel = Channel.Create("Детский", "kids", T0);
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
channel.SetTemplate(template.Id);
|
||||
|
||||
await using var seed = fixture.New();
|
||||
seed.MediaAssets.AddRange(assets);
|
||||
seed.Shows.Add(show);
|
||||
seed.Groups.Add(group);
|
||||
seed.Channels.Add(channel);
|
||||
seed.ScheduleTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
return channel.Id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user