Enhance grid import/export functionality to support dynamic groups and collections
ci / build-backend (push) Successful in 1m43s
ci / build-frontend (push) Successful in 58s
ci / tests (push) Successful in 4m14s
ci / sonar (push) Successful in 4m38s

Updated the grid import/export process to allow for the inclusion of groups and collections directly from the configuration file. This enhancement ensures that missing groups and collections are created during import, improving the flexibility of the grid management system. Adjusted related classes and methods to accommodate these changes, ensuring a cohesive integration. Enhanced documentation and user prompts to clarify the new functionality and its usage.
This commit is contained in:
Leonid Pershin
2026-07-28 02:23:43 +03:00
parent f7e7b5f7c3
commit fe11034b16
14 changed files with 539 additions and 28 deletions
@@ -95,7 +95,7 @@ public class GridTransferTests
private static async Task<GridConfig> ExportAsync(TestDb fixture, Guid channelId)
{
await using var db = fixture.New();
var result = await new ExportGridQueryHandler(db).Handle(
var result = await new ExportGridQueryHandler(db, GroupServices.Dynamic(db)).Handle(
new ExportGridQuery(channelId),
CancellationToken.None
);
@@ -111,7 +111,7 @@ public class GridTransferTests
)
{
await using var db = fixture.New();
var result = await new ImportGridCommandHandler(db, new SlotWriter(db)).Handle(
var result = await new ImportGridCommandHandler(db, new SlotWriter(db), Stats(db)).Handle(
new ImportGridCommand(channelId, config, replace),
CancellationToken.None
);
@@ -291,6 +291,179 @@ public class GridTransferTests
Assert.True(stored.HasPendingChanges);
}
[Fact]
public async Task Import_CreatesGroupsFromShows()
{
// Сетка, собранная ИИ под ещё не разобранную библиотеку: групп нет, и положить слоты
// некуда — значит файл приносит группы с собой.
var fixture = new TestDb();
var channelId = await SeedAsync(fixture, withSlot: false);
var config = new GridConfig(
[
new GridConfigLayer(
"Основная сетка",
10,
[
new GridConfigSlot(
"Вечерний блок",
new TimeOnly(20, 0),
120,
Daypart.Prime,
Group: "Вечерние сериалы"
),
]
),
],
Groups: [new GridConfigGroup("Вечерние сериалы", Shows: ["Сериал"])]
);
var imported = await ImportAsync(fixture, channelId, config);
Assert.Equal(1, imported.Groups);
Assert.Equal(1, imported.Slots);
Assert.Empty(imported.Warnings);
await using var db = fixture.New();
var group = await db
.Groups.Include(g => g.Items)
.FirstAsync(g => g.Name == "Вечерние сериалы", CancellationToken.None);
Assert.Single(group.Items);
// Статистика — кэш карточки группы, и пустой после создания она быть не должна.
Assert.Equal(1, group.UnitCount);
}
[Fact]
public async Task Import_CreatesCollections_AndPutsThemIntoGroups()
{
var fixture = new TestDb();
var channelId = await SeedAsync(fixture, withSlot: false);
var config = new GridConfig(
[
new GridConfigLayer(
"Основная сетка",
10,
[
new GridConfigSlot(
"Кино",
new TimeOnly(20, 0),
120,
Daypart.Prime,
Group: "Циклы"
),
]
),
],
Collections: [new GridConfigCollection("Франшиза", ["Сериал"])],
Groups: [new GridConfigGroup("Циклы", Collections: ["Франшиза"])]
);
var imported = await ImportAsync(fixture, channelId, config);
Assert.Equal(1, imported.Collections);
Assert.Equal(1, imported.Groups);
Assert.Empty(imported.Warnings);
await using var db = fixture.New();
var collection = await db
.Collections.Include(c => c.Items)
.FirstAsync(c => c.Name == "Франшиза", CancellationToken.None);
Assert.Single(collection.Items);
var group = await db
.Groups.Include(g => g.Items)
.FirstAsync(g => g.Name == "Циклы", CancellationToken.None);
var item = Assert.Single(group.Items);
Assert.Equal(GroupElementKind.Collection, item.ElementKind);
Assert.Equal(collection.Id, item.ElementId);
}
[Fact]
public async Task Import_DoesNotTouchExistingGroup()
{
// Группа с этим именем уже стоит в слотах соседнего канала: переписать её состав чужим
// файлом значит молча перекроить чужой эфир.
var fixture = new TestDb();
var channelId = await SeedAsync(fixture, withSlot: false);
var config = new GridConfig(
[new GridConfigLayer("Основная сетка", 10, [])],
Groups: [new GridConfigGroup("Сериалы", Shows: ["Сериал", "Сериал"])]
);
var imported = await ImportAsync(fixture, channelId, config);
Assert.Equal(0, imported.Groups);
await using var db = fixture.New();
var group = await db
.Groups.Include(g => g.Items)
.FirstAsync(g => g.Name == "Сериалы", CancellationToken.None);
Assert.Single(group.Items);
}
[Fact]
public async Task Import_WarnsAboutUnknownShow_AndEmptyGroup()
{
var fixture = new TestDb();
var channelId = await SeedAsync(fixture, withSlot: false);
var config = new GridConfig(
[new GridConfigLayer("Основная сетка", 10, [])],
Groups: [new GridConfigGroup("Придуманное", Shows: ["Шоу, которого нет"])]
);
var imported = await ImportAsync(fixture, channelId, config);
Assert.Equal(1, imported.Groups);
Assert.Contains(
imported.Warnings,
w => w.Contains("Шоу, которого нет", StringComparison.Ordinal)
);
// Пустая группа — это дыра в эфире, и сказать об этом надо на импорте, а не после эфира.
Assert.Contains(
imported.Warnings,
w => w.Contains("создана пустой", StringComparison.Ordinal)
);
}
[Fact]
public async Task Export_CarriesCompositionOfUsedGroups()
{
var fixture = new TestDb();
var channelId = await SeedAsync(fixture);
var config = await ExportAsync(fixture, channelId);
// Файл должен быть самодостаточным: на другой установке этих групп нет, а слот без группы
// положить некуда.
var group = Assert.Single(config.Groups ?? []);
Assert.Equal("Сериалы", group.Name);
Assert.Equal(["Сериал"], group.Shows);
}
[Fact]
public async Task Prompt_ExplainsHowToBuildGroups()
{
var fixture = new TestDb();
var channelId = await SeedAsync(fixture);
await using var db = fixture.New();
var result = await new BuildGridPromptQueryHandler(db, Catalog(db)).Handle(
new BuildGridPromptQuery(channelId, [], null),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var prompt = result.Value.Prompt;
// Модель должна знать и что группы можно собрать самой, и что названия шоу берутся дословно.
Assert.Contains("\"groups\"", prompt, StringComparison.Ordinal);
Assert.Contains("\"collections\"", prompt, StringComparison.Ordinal);
Assert.Contains("дословно", prompt, StringComparison.Ordinal);
}
[Fact]
public async Task Prompt_ListsGroupsAndSchema()
{
@@ -375,4 +548,7 @@ public class GridTransferTests
private static GroupCatalog Catalog(IAppDbContext db) =>
new(db, GroupServices.Dynamic(db), new GroupElementResolver(db));
private static GroupStatsService Stats(IAppDbContext db) =>
new(db, new GroupElementResolver(db), GroupServices.Dynamic(db));
}