комит

This commit is contained in:
dipatrik10
2026-07-27 00:33:16 +03:00
parent 92ff87a917
commit 7e29c53ce9
21 changed files with 890 additions and 91 deletions

View File

@@ -114,7 +114,8 @@ public class AcademicCalendarGridService {
private void validateWeek(AcademicYear academicYear, LocalDate date, Integer weekNumber) {
long daysFromYearStart = ChronoUnit.DAYS.between(academicYear.getStartDate(), date);
int expectedWeek = Math.toIntExact(daysFromYearStart / 7 + 1);
int daysBeforeYearStartInFirstWeek = academicYear.getStartDate().getDayOfWeek().getValue() - 1;
int expectedWeek = Math.toIntExact((daysBeforeYearStartInFirstWeek + daysFromYearStart) / 7 + 1);
if (weekNumber != expectedWeek) {
throw new IllegalArgumentException("Номер недели не соответствует дате учебного года");
}

View File

@@ -0,0 +1,23 @@
-- Недели календарного учебного графика выровнены по ISO-неделе:
-- понедельник — первый день, воскресенье — последний.
UPDATE academic_calendar_days AS calendar_day
SET week_number = (
(
calendar_day.date
- academic_year.start_date
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
- 1
) / 7
) + 1
FROM academic_calendars AS calendar
JOIN academic_years AS academic_year
ON academic_year.id = calendar.academic_year_id
WHERE calendar_day.calendar_id = calendar.id
AND calendar_day.week_number IS DISTINCT FROM (
(
calendar_day.date
- academic_year.start_date
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
- 1
) / 7
) + 1;

View File

@@ -0,0 +1,218 @@
package com.magistr.app.migration;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationVersion;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@DisplayName("Миграция нумерации недель календарного графика")
class AcademicCalendarWeekNumberMigrationIntegrationTest {
@Container
static final PostgreSQLContainer<?> POSTGRES =
new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);
@Test
@DisplayName("V2 переносит первый понедельник после неполной недели в неделю 2")
void migrationAlignsWeeksToMondayFor2026And2027() throws SQLException {
Flyway baseline = flyway("1");
baseline.clean();
baseline.migrate();
long calendar2026;
long calendar2027;
try (Connection connection = POSTGRES.createConnection("")) {
SeedReferences references = loadSeedReferences(connection);
long year2026 = insertAcademicYear(
connection,
"2026-2027",
LocalDate.of(2026, 9, 1),
LocalDate.of(2027, 6, 30)
);
calendar2026 = insertCalendar(connection, year2026, references, "График 2026-2027");
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
LocalDate.of(2026, 9, 1), 1, 2);
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
LocalDate.of(2026, 9, 7), 1, 1);
long year2027 = insertAcademicYear(
connection,
"2027-2028",
LocalDate.of(2027, 9, 1),
LocalDate.of(2028, 6, 30)
);
calendar2027 = insertCalendar(connection, year2027, references, "График 2027-2028");
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
LocalDate.of(2027, 9, 1), 1, 3);
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
LocalDate.of(2027, 9, 6), 1, 1);
}
Flyway latest = latestFlyway();
latest.migrate();
assertThat(latest.info().current()).isNotNull();
assertThat(latest.info().current().getVersion().getVersion()).isEqualTo("2");
try (Connection connection = POSTGRES.createConnection("")) {
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 1))).isEqualTo(1);
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 7))).isEqualTo(2);
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 1))).isEqualTo(1);
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 6))).isEqualTo(2);
}
}
private Flyway flyway(String targetVersion) {
return Flyway.configure()
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
.locations("classpath:db/migration")
.target(MigrationVersion.fromVersion(targetVersion))
.cleanDisabled(false)
.load();
}
private Flyway latestFlyway() {
return Flyway.configure()
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
.locations("classpath:db/migration")
.cleanDisabled(false)
.load();
}
private SeedReferences loadSeedReferences(Connection connection) throws SQLException {
long specialtyId = queryLong(connection, "SELECT id FROM specialties ORDER BY id LIMIT 1");
long profileId = queryLong(
connection,
"SELECT id FROM specialty_profiles WHERE specialty_id = ? ORDER BY id LIMIT 1",
specialtyId
);
long studyFormId = queryLong(connection, "SELECT id FROM education_forms ORDER BY id LIMIT 1");
long activityTypeId = queryLong(
connection,
"SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'"
);
return new SeedReferences(specialtyId, profileId, studyFormId, activityTypeId);
}
private long insertAcademicYear(Connection connection,
String title,
LocalDate startDate,
LocalDate endDate) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("""
INSERT INTO academic_years (title, start_date, end_date)
VALUES (?, ?, ?)
RETURNING id
""")) {
statement.setString(1, title);
statement.setDate(2, Date.valueOf(startDate));
statement.setDate(3, Date.valueOf(endDate));
return requiredLong(statement);
}
}
private long insertCalendar(Connection connection,
long academicYearId,
SeedReferences references,
String title) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("""
INSERT INTO academic_calendars (
title,
academic_year_id,
specialty_id,
specialty_profile_id,
study_form_id,
course_count
)
VALUES (?, ?, ?, ?, ?, 1)
RETURNING id
""")) {
statement.setString(1, title);
statement.setLong(2, academicYearId);
statement.setLong(3, references.specialtyId());
statement.setLong(4, references.profileId());
statement.setLong(5, references.studyFormId());
return requiredLong(statement);
}
}
private void insertCalendarDay(Connection connection,
long calendarId,
long activityTypeId,
LocalDate date,
int weekNumber,
int dayOfWeek) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("""
INSERT INTO academic_calendar_days (
calendar_id,
course_number,
date,
week_number,
day_of_week,
activity_type_id
)
VALUES (?, 1, ?, ?, ?, ?)
""")) {
statement.setLong(1, calendarId);
statement.setDate(2, Date.valueOf(date));
statement.setInt(3, weekNumber);
statement.setInt(4, dayOfWeek);
statement.setLong(5, activityTypeId);
statement.executeUpdate();
}
}
private int findWeekNumber(Connection connection,
long calendarId,
LocalDate date) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("""
SELECT week_number
FROM academic_calendar_days
WHERE calendar_id = ?
AND date = ?
""")) {
statement.setLong(1, calendarId);
statement.setDate(2, Date.valueOf(date));
try (ResultSet resultSet = statement.executeQuery()) {
assertThat(resultSet.next()).isTrue();
return resultSet.getInt(1);
}
}
}
private long queryLong(Connection connection, String sql, Object... arguments) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (int index = 0; index < arguments.length; index++) {
statement.setObject(index + 1, arguments[index]);
}
return requiredLong(statement);
}
}
private long requiredLong(PreparedStatement statement) throws SQLException {
try (ResultSet resultSet = statement.executeQuery()) {
assertThat(resultSet.next()).isTrue();
return resultSet.getLong(1);
}
}
private record SeedReferences(
long specialtyId,
long profileId,
long studyFormId,
long activityTypeId
) {
}
}

View File

@@ -49,10 +49,11 @@ class AcademicCalendarGridServiceTest {
private AcademicCalendarGridService service;
private AcademicCalendarActivityType activityType;
private AcademicYear academicYear;
@BeforeEach
void setUp() {
AcademicYear academicYear = new AcademicYear();
academicYear = new AcademicYear();
academicYear.setId(1L);
academicYear.setTitle("2025-2026");
academicYear.setStartDate(YEAR_START);
@@ -197,6 +198,48 @@ class AcademicCalendarGridServiceTest {
});
}
@Test
void alignsFirstWeekToMondayWhenAcademicYearStartsOnTuesday() {
LocalDate yearStart = LocalDate.of(2026, 9, 1);
academicYear.setStartDate(yearStart);
academicYear.setEndDate(LocalDate.of(2027, 6, 30));
service.replaceGrid(CALENDAR_ID, List.of(
validRow(yearStart, 1, 2),
validRow(LocalDate.of(2026, 9, 6), 1, 7),
validRow(LocalDate.of(2026, 9, 7), 2, 1),
validRow(LocalDate.of(2026, 9, 14), 3, 1)
));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AcademicCalendarDay>> replacementCaptor = ArgumentCaptor.forClass(List.class);
verify(calendarDayRepository).saveAll(replacementCaptor.capture());
assertThat(replacementCaptor.getValue())
.extracting(AcademicCalendarDay::getWeekNumber)
.containsExactly(1, 1, 2, 3);
}
@Test
void alignsFirstWeekToMondayWhenAcademicYearStartsOnWednesday() {
LocalDate yearStart = LocalDate.of(2027, 9, 1);
academicYear.setStartDate(yearStart);
academicYear.setEndDate(LocalDate.of(2028, 6, 30));
service.replaceGrid(CALENDAR_ID, List.of(
validRow(yearStart, 1, 3),
validRow(LocalDate.of(2027, 9, 5), 1, 7),
validRow(LocalDate.of(2027, 9, 6), 2, 1),
validRow(LocalDate.of(2027, 9, 13), 3, 1)
));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AcademicCalendarDay>> replacementCaptor = ArgumentCaptor.forClass(List.class);
verify(calendarDayRepository).saveAll(replacementCaptor.capture());
assertThat(replacementCaptor.getValue())
.extracting(AcademicCalendarDay::getWeekNumber)
.containsExactly(1, 1, 2, 3);
}
private AcademicCalendarGridDayDto validRow(LocalDate date, int weekNumber, int dayOfWeek) {
return row(date, weekNumber, dayOfWeek, ACTIVITY_TYPE_ID);
}