Enhance school creation and staffing management with native language support
- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options. - Enhanced the UI for school creation to allow selection of native languages, improving user experience. - Revised API interfaces to accommodate new native language features, ensuring proper data handling. - Improved localization strings to support new native language functionalities in both English and Russian. - Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
This commit is contained in:
@@ -160,6 +160,11 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
|
||||
Assert.Equal("Славянский", Assert.Single(ru.NameSets, set => set.DefName == "Slavic").Label);
|
||||
Assert.Equal("Slavic", Assert.Single(en.NameSets, set => set.DefName == "Slavic").Label);
|
||||
var slavic = Assert.Single(ru.NameSets, set => set.DefName == "Slavic");
|
||||
Assert.Equal(
|
||||
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||
slavic.NativeLanguages.Select(language => language.DefName).ToArray());
|
||||
Assert.Equal("Белорусский", Assert.Single(slavic.NativeLanguages, language => language.DefName == "BelarusianLanguage").Label);
|
||||
Assert.Equal("Начальные классы", Assert.Single(ru.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
||||
Assert.Equal("Primary", Assert.Single(en.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
||||
var classroom = Assert.Single(ru.Rooms, room => room.DefName == "Classroom");
|
||||
@@ -287,6 +292,27 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
Assert.Equal("unknown-name-set", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_WithAnUnknownNativeLanguage_IsRejected()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
"/api/schools",
|
||||
new
|
||||
{
|
||||
name = "Чужой язык",
|
||||
startDate = ExpectedDefaultStart,
|
||||
nameSetId = "Slavic",
|
||||
nativeLanguage = "Klingon",
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("unknown-native-language", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_WithACustomConnectedMap_Succeeds()
|
||||
{
|
||||
@@ -417,7 +443,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
IReadOnlyList<DefInfoResponse> Floors,
|
||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||
IReadOnlyList<DefInfoResponse> Things,
|
||||
IReadOnlyList<DefInfoResponse> NameSets,
|
||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||
DayFrameResponse? DayFrame,
|
||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||
@@ -425,6 +451,11 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
|
||||
private sealed record DefInfoResponse(string DefName, string Label);
|
||||
|
||||
private sealed record NameSetInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
||||
|
||||
private sealed record RoomInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
|
||||
@@ -29,14 +29,23 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
Assert.Equal(100_000f, staffing.Allocated);
|
||||
Assert.Equal(0f, staffing.Payroll);
|
||||
Assert.Equal(100_000f, staffing.Remaining);
|
||||
Assert.Equal(12, staffing.Applicants.Count);
|
||||
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");
|
||||
Assert.NotEmpty(staffing.Positions);
|
||||
Assert.Contains(staffing.Positions, position => position.DefName == "Teacher");
|
||||
Assert.Contains(staffing.Subjects, subject => subject.DefName == "Mathematics");
|
||||
Assert.All(staffing.Applicants, applicant => Assert.NotEmpty(applicant.Skills));
|
||||
Assert.All(
|
||||
staffing.Applicants,
|
||||
applicant =>
|
||||
{
|
||||
Assert.Contains(applicant.Skills, skill => skill.Id == "Communication");
|
||||
Assert.Contains(
|
||||
applicant.Skills,
|
||||
skill => skill.Id is "RussianLanguage" or "BelarusianLanguage" or "UkrainianLanguage");
|
||||
Assert.All(applicant.Skills, skill => Assert.False(string.IsNullOrWhiteSpace(skill.Value)));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -135,6 +144,12 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = applicant.Id, position = "Teacher" },
|
||||
TestContext.Current.CancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
Assert.Equal("payroll-exceeded", await ProblemCodeAsync(response));
|
||||
break;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
@@ -143,7 +158,7 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
Assert.Equal(100_000f, staffing.Allocated);
|
||||
Assert.True(staffing.Payroll > 0f);
|
||||
Assert.True(staffing.Payroll <= staffing.Allocated);
|
||||
Assert.Equal(12, staffing.Staff.Count);
|
||||
Assert.NotEmpty(staffing.Staff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -224,9 +239,23 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
var staffing = await GetStaffingAsync(client, school.Id);
|
||||
while (staffing.Applicants.Count > 0)
|
||||
{
|
||||
staffing = await HireAsync(client, school.Id, staffing.Applicants[0].Id, "Teacher");
|
||||
var applicant = staffing.Applicants[0];
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = applicant.Id, position = "Teacher" },
|
||||
TestContext.Current.CancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
}
|
||||
|
||||
Assert.NotEmpty(staffing.Staff);
|
||||
|
||||
var subjects = staffing.Subjects.Select(subject => subject.DefName).ToArray();
|
||||
Assert.NotEmpty(subjects);
|
||||
|
||||
@@ -327,7 +356,8 @@ public class StaffingApiTests(AppHostFixture fixture)
|
||||
bool IsParent,
|
||||
float HourlyWageAsk,
|
||||
float MonthlyBase,
|
||||
IReadOnlyList<LabeledStatResponse> Skills);
|
||||
IReadOnlyList<LabeledStatResponse> Skills,
|
||||
IReadOnlyList<DefLabelResponse> Traits);
|
||||
|
||||
private sealed record LabeledStatResponse(string Id, string Label, string Value);
|
||||
|
||||
|
||||
@@ -118,6 +118,73 @@ public class TimetableApiTests(AppHostFixture fixture)
|
||||
&& lesson.Locked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The refusal codes the client switches on. `pin-rejected` is covered above; these five are
|
||||
/// the rest of the documented contract, and none of them was exercised.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task PinAndUnpinRefusals_CarryTheDocumentedCodes()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Расписание отказы", TuesdayMorning);
|
||||
|
||||
var empty = await GetTimetableAsync(client, school.Id);
|
||||
var klass = empty.Classes[0];
|
||||
var room = empty.Rooms.First(candidate => candidate.Id.StartsWith("classroom-", StringComparison.Ordinal));
|
||||
|
||||
// Nobody teaches anything yet, so a well-formed pin still has no teacher to place.
|
||||
Assert.Equal("no-teacher", await PinCodeAsync(client, school.Id, klass.Id, "Mathematics", room.Id));
|
||||
Assert.Equal("unknown-class", await PinCodeAsync(client, school.Id, "cZZ", "Mathematics", room.Id));
|
||||
Assert.Equal("unknown-subject", await PinCodeAsync(client, school.Id, klass.Id, "Astrology", room.Id));
|
||||
Assert.Equal("unknown-room", await PinCodeAsync(client, school.Id, klass.Id, "Mathematics", "no-such-room"));
|
||||
|
||||
await HireMathAsync(client, school.Id);
|
||||
var table = await GetTimetableAsync(client, school.Id);
|
||||
var lesson = table.Lessons.First(row => row.Subject == "Mathematics");
|
||||
|
||||
using var pin = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin",
|
||||
new { lesson.ClassId, lesson.Subject, lesson.RoomId, lesson.Day, lesson.Period },
|
||||
TestContext.Current.CancellationToken);
|
||||
pin.EnsureSuccessStatusCode();
|
||||
|
||||
var unpinQuery = $"classId={Uri.EscapeDataString(lesson.ClassId)}&subject={lesson.Subject}"
|
||||
+ $"&day={lesson.Day}&period={lesson.Period}";
|
||||
using var unpin = await client.DeleteAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin?{unpinQuery}",
|
||||
TestContext.Current.CancellationToken);
|
||||
unpin.EnsureSuccessStatusCode();
|
||||
|
||||
// The lock is gone; asking again is a 404, not a silent success.
|
||||
using var again = await client.DeleteAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin?{unpinQuery}",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, again.StatusCode);
|
||||
Assert.Equal("unknown-lesson", await ProblemCodeAsync(again));
|
||||
|
||||
using var incomplete = await client.DeleteAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin?classId={Uri.EscapeDataString(lesson.ClassId)}",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, incomplete.StatusCode);
|
||||
Assert.Equal("invalid-query", await ProblemCodeAsync(incomplete));
|
||||
}
|
||||
|
||||
private static async Task<string?> PinCodeAsync(
|
||||
HttpClient client,
|
||||
int schoolId,
|
||||
string classId,
|
||||
string subject,
|
||||
string roomId)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{schoolId}/timetable/pin",
|
||||
new { classId, subject, roomId, day = 0, period = 1 },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.False(response.IsSuccessStatusCode, $"pinning {subject} into {roomId} was expected to fail.");
|
||||
return await ProblemCodeAsync(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Saturday_HasNoOccupancyOnTheMap()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user