комит
This commit is contained in:
@@ -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("Номер недели не соответствует дате учебного года");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user