Enhance staffing management with uncovered teacher tracking
- Updated `protocol.md` to clarify the concept of uncovered subjects and the number of additional teachers required. - Introduced `teachersShort` property in the `StaffingSubject` interface to indicate how many more teachers are needed for a subject. - Enhanced localization strings to reflect the new uncovered teacher information in both English and Russian. - Updated the management panel UI to display the number of teachers short for each uncovered subject. - Revised the `Uncovered` method in the staffing logic to return detailed information about uncovered subjects, including the shortfall of teachers. - Added tests to validate the new uncovered teacher tracking functionality and ensure accurate reporting in the staffing API. - Marked related tasks as complete in the documentation for the golden fixtures phase.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class GoldenSaveTests(AppHostFixture fixture)
|
||||
{
|
||||
[Fact]
|
||||
public async Task CurrentFormatSave_LoadsTheSamePeopleAndTime()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
await InstallAsync(client, "current");
|
||||
|
||||
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
||||
var school = Assert.Single(state.Schools);
|
||||
Assert.Equal(1, school.Id);
|
||||
Assert.Equal("Золотая", school.Name);
|
||||
Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime);
|
||||
Assert.False(school.Running);
|
||||
|
||||
var people = await PeopleAsync(client, school.Id);
|
||||
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
|
||||
Assert.Contains(people, person => person.Roles.Contains("student"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveWithoutNativeLanguage_LoadsWithoutReshuffling()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
await InstallAsync(client, "legacy-no-native");
|
||||
|
||||
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
||||
var school = Assert.Single(state.Schools);
|
||||
Assert.Equal("Золотая", school.Name);
|
||||
|
||||
var people = await PeopleAsync(client, school.Id);
|
||||
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
private async Task InstallAsync(HttpClient client, string folder)
|
||||
{
|
||||
var directory = await SavesDirectoryAsync(client);
|
||||
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
|
||||
foreach (var file in Directory.EnumerateFiles(source))
|
||||
{
|
||||
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
|
||||
}
|
||||
|
||||
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||
reload.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private static async Task<string> SavesDirectoryAsync(HttpClient client)
|
||||
{
|
||||
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
|
||||
"/api/dev/saves-directory",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
|
||||
return payload.Path;
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var page = await client.GetFromJsonAsync<PeoplePage>(
|
||||
$"/api/schools/{schoolId}/people?pageSize=100",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(page);
|
||||
return page.People
|
||||
.OrderBy(row => row.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] ExpectedNames()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "golden", "current", "1.people.json"));
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.GetProperty("people")
|
||||
.EnumerateArray()
|
||||
.Select(person =>
|
||||
{
|
||||
var name = person.GetProperty("name");
|
||||
var surname = name.GetProperty("surname").GetString() ?? "";
|
||||
var given = name.GetProperty("given").GetString() ?? "";
|
||||
var patronymic = name.GetProperty("patronymic").GetString() ?? "";
|
||||
return string.Join(' ', new[] { surname, given, patronymic }.Where(part => part.Length > 0));
|
||||
})
|
||||
.OrderBy(full => full, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private sealed record SavesDirectoryResponse(string Path);
|
||||
|
||||
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
|
||||
|
||||
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
|
||||
}
|
||||
@@ -26,4 +26,10 @@
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="golden\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -32,7 +32,8 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
Assert.Equal(32, staffing.Applicants.Count);
|
||||
Assert.Empty(staffing.Staff);
|
||||
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "Mathematics");
|
||||
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "PrimarySchool");
|
||||
var primary = Assert.Single(staffing.Uncovered, subject => subject.DefName == "PrimarySchool");
|
||||
Assert.Equal(3, primary.TeachersShort);
|
||||
Assert.NotEmpty(staffing.Positions);
|
||||
Assert.Contains(staffing.Positions, position => position.DefName == "Teacher");
|
||||
Assert.Contains(staffing.Subjects, subject => subject.DefName == "Mathematics");
|
||||
@@ -331,7 +332,7 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
IReadOnlyList<DefLabelResponse> Positions,
|
||||
IReadOnlyList<UncoveredSubjectResponse> Subjects);
|
||||
|
||||
private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek);
|
||||
private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek, int TeachersShort);
|
||||
|
||||
private sealed record ApplicantResponse(
|
||||
string Id,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Golden saves
|
||||
|
||||
`current/` is a school file of today's format. `legacy-no-native/` is the same school without
|
||||
`nativeLanguage`, so a missing field still has to mean "first language of the name set" rather
|
||||
than a new roll.
|
||||
|
||||
These files live forever: a new save field must load `current/` and must not reshuffle
|
||||
`legacy-no-native/`. A real format change adds a new folder; it does not rewrite the old one.
|
||||
|
||||
Regenerate after an intentional roster change (same commit):
|
||||
|
||||
```
|
||||
WRITE_GOLDEN=1 dotnet test tests/HSchool.People.Tests/HSchool.People.Tests.csproj --filter Dump_HostSaveFixtures
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"format": 2,
|
||||
"id": 1,
|
||||
"name": "Золотая",
|
||||
"gameTime": "2012-03-31T06:00:00Z",
|
||||
"running": false,
|
||||
"speedIndex": 1,
|
||||
"modIds": [
|
||||
"core"
|
||||
],
|
||||
"map": {
|
||||
"territory": {
|
||||
"id": "yard",
|
||||
"def": "SchoolYard"
|
||||
},
|
||||
"buildings": [
|
||||
{
|
||||
"id": "main",
|
||||
"def": "MainBuilding"
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"id": "floor-1",
|
||||
"def": "StandardFloor",
|
||||
"building": "main",
|
||||
"label": "1"
|
||||
}
|
||||
],
|
||||
"rooms": [
|
||||
{
|
||||
"id": "classroom-00",
|
||||
"def": "Classroom",
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "101",
|
||||
"seats": 16,
|
||||
"slots": []
|
||||
},
|
||||
{
|
||||
"id": "classroom-01",
|
||||
"def": "Classroom",
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "102",
|
||||
"seats": 16,
|
||||
"slots": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"a": "yard",
|
||||
"b": "classroom-00"
|
||||
},
|
||||
{
|
||||
"a": "yard",
|
||||
"b": "classroom-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nameSetId": "Slavic",
|
||||
"nativeLanguage": "RussianLanguage"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"format": 2,
|
||||
"id": 1,
|
||||
"name": "Золотая",
|
||||
"gameTime": "2012-03-31T06:00:00Z",
|
||||
"running": false,
|
||||
"speedIndex": 1,
|
||||
"modIds": [
|
||||
"core"
|
||||
],
|
||||
"map": {
|
||||
"territory": {
|
||||
"id": "yard",
|
||||
"def": "SchoolYard"
|
||||
},
|
||||
"buildings": [
|
||||
{
|
||||
"id": "main",
|
||||
"def": "MainBuilding"
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"id": "floor-1",
|
||||
"def": "StandardFloor",
|
||||
"building": "main",
|
||||
"label": "1"
|
||||
}
|
||||
],
|
||||
"rooms": [
|
||||
{
|
||||
"id": "classroom-00",
|
||||
"def": "Classroom",
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "101",
|
||||
"seats": 16,
|
||||
"slots": []
|
||||
},
|
||||
{
|
||||
"id": "classroom-01",
|
||||
"def": "Classroom",
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "102",
|
||||
"seats": 16,
|
||||
"slots": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"a": "yard",
|
||||
"b": "classroom-00"
|
||||
},
|
||||
{
|
||||
"a": "yard",
|
||||
"b": "classroom-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nameSetId": "Slavic"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user