много всего и тестовые данные
This commit is contained in:
@@ -62,6 +62,20 @@ public class AcademicCalendarController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<AcademicCalendarDto> getCalendarsPage(
|
||||
@RequestParam(defaultValue = "") String query,
|
||||
@RequestParam(required = false) Long academicYearId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "25") int size) {
|
||||
return PageResponse.from(calendarRepository.searchOptions(
|
||||
query.trim().toLowerCase(Locale.ROOT), academicYearId, null, null, null,
|
||||
PageRequest.of(Math.max(0, page), Math.max(1, Math.min(size, 100)),
|
||||
Sort.by("academicYear.startDate").descending().and(Sort.by("title", "id"))))
|
||||
.map(this::toCalendarDto));
|
||||
}
|
||||
|
||||
@GetMapping("/options")
|
||||
public List<AcademicCalendarDto> searchCalendarOptions(
|
||||
@RequestParam(defaultValue = "") String query,
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleRuleDto;
|
||||
import com.magistr.app.dto.PageResponse;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleRuleConflictException;
|
||||
import com.magistr.app.service.ScheduleRuleService;
|
||||
@@ -39,6 +40,15 @@ public class ScheduleRuleAdminController {
|
||||
return scheduleRuleService.getAll(semesterId, groupId, versionId);
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
public PageResponse<ScheduleRuleDto> getPage(
|
||||
@RequestParam Long versionId,
|
||||
@RequestParam(defaultValue = "") String query,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "25") int size) {
|
||||
return scheduleRuleService.getPage(versionId, query, page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ScheduleRuleDto> getById(@PathVariable("id") Long id) {
|
||||
return ResponseEntity.ok(scheduleRuleService.getById(id));
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.magistr.app.repository;
|
||||
import com.magistr.app.model.ScheduleRule;
|
||||
import jakarta.persistence.LockModeType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -13,6 +15,23 @@ import java.util.Collection;
|
||||
|
||||
public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long> {
|
||||
|
||||
// Сначала ограничиваем ID в БД, затем загружаем коллекции только выбранной страницы.
|
||||
@Query("""
|
||||
select r.id from ScheduleRule r
|
||||
join r.scheduleVersion version
|
||||
join r.subject subject
|
||||
where r.status <> 'ARCHIVED'
|
||||
and version.id = :versionId
|
||||
and (:query = '' or str(r.id) = :query
|
||||
or lower(subject.name) like concat('%', :query, '%')
|
||||
or exists (select g.id from ScheduleRule matchingRule join matchingRule.groups g
|
||||
where matchingRule.id = r.id and lower(g.name) like concat('%', :query, '%')))
|
||||
order by r.id desc
|
||||
""")
|
||||
Page<Long> searchPageIds(
|
||||
@Param("versionId") Long versionId, @Param("query") String query,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
@@ -210,4 +229,23 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
|
||||
where r.id = :id
|
||||
""")
|
||||
Optional<ScheduleRule> findByIdWithDetails(@Param("id") Long id);
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
left join fetch r.subject
|
||||
left join fetch r.semester sem
|
||||
left join fetch sem.academicYear
|
||||
left join fetch r.scheduleVersion
|
||||
left join fetch r.groups groups
|
||||
left join fetch r.slots slots
|
||||
left join fetch slots.timeSlot
|
||||
left join fetch slots.subgroups slotSubgroups
|
||||
left join fetch slotSubgroups.studentGroup
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
where r.id in :ids
|
||||
""")
|
||||
List<ScheduleRule> findPageWithDetails(@Param("ids") Collection<Long> ids);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.dto.ScheduleRuleDto;
|
||||
import com.magistr.app.dto.PageResponse;
|
||||
import com.magistr.app.dto.ScheduleRuleSlotDto;
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.model.Classroom;
|
||||
@@ -31,6 +32,7 @@ import com.magistr.app.repository.SubgroupRepository;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -46,6 +48,7 @@ import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
@@ -117,6 +120,20 @@ public class ScheduleRuleService {
|
||||
this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<ScheduleRuleDto> getPage(Long versionId, String query, int page, int size) {
|
||||
var ids = scheduleRuleRepository.searchPageIds(versionId,
|
||||
query == null ? "" : query.trim().toLowerCase(Locale.ROOT),
|
||||
PageRequest.of(Math.max(0, page), Math.max(1, Math.min(size, 100))));
|
||||
if (ids.isEmpty()) {
|
||||
return new PageResponse<>(List.of(), ids.getNumber(), ids.getSize(), ids.getTotalElements(),
|
||||
ids.getTotalPages(), ids.isFirst(), ids.isLast());
|
||||
}
|
||||
var details = scheduleRuleRepository.findPageWithDetails(ids.getContent()).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(ScheduleRule::getId, this::toDto));
|
||||
return PageResponse.from(ids.map(details::get));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ScheduleRuleDto> getAll(Long semesterId, Long groupId) {
|
||||
return getAll(semesterId, groupId, null);
|
||||
|
||||
@@ -19,11 +19,6 @@ CREATE TABLE IF NOT EXISTS departments (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO departments (name, code) VALUES
|
||||
('Кафедра ИБ', 1),
|
||||
('Кафедра ВТ', 2),
|
||||
('Кафедра КТ', 3);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS specialties (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
@@ -36,12 +31,6 @@ CREATE TABLE IF NOT EXISTS specialties (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO specialties (name, specialty_code) VALUES
|
||||
('Информационная безопасность', '10.03.01'),
|
||||
('Информатика и вычислительная техника', '09.03.01'),
|
||||
('Программная инженерия', '09.03.04')
|
||||
ON CONFLICT (specialty_code) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS specialty_profiles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
specialty_id BIGINT NOT NULL REFERENCES specialties(id) ON DELETE CASCADE,
|
||||
@@ -57,11 +46,6 @@ CREATE TABLE IF NOT EXISTS specialty_profiles (
|
||||
CONSTRAINT uq_specialty_profiles_name UNIQUE (specialty_id, name)
|
||||
);
|
||||
|
||||
INSERT INTO specialty_profiles (specialty_id, name)
|
||||
SELECT id, 'Без профиля'
|
||||
FROM specialties
|
||||
ON CONFLICT (specialty_id, name) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Пользователи и роли
|
||||
-- ==========================================
|
||||
@@ -83,15 +67,6 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
-- Админ по умолчанию: admin / admin (bcrypt через pgcrypto)
|
||||
INSERT INTO users (username, password, role, full_name, job_title, department_id)
|
||||
VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN', 'Иванов Админ Иванович', 'Доцент', 1),
|
||||
('Тестовый преподаватель', crypt('1234567890', gen_salt('bf', 10)), 'TEACHER', 'Петров Препод Петрович', 'Профессор', 2),
|
||||
('учебный_отдел', crypt('1234567890', gen_salt('bf', 10)), 'EDUCATION_OFFICE', 'Оператор учебного отдела', 'Учебный отдел', 1),
|
||||
('кафедра_иб', crypt('1234567890', gen_salt('bf', 10)), 'DEPARTMENT', 'Оператор кафедры ИБ', 'Кафедра', 1),
|
||||
('просмотр_расписаний', crypt('1234567890', gen_salt('bf', 10)), 'SCHEDULE_VIEWER', 'Оператор просмотра расписаний', 'Просмотр расписаний', 1)
|
||||
ON CONFLICT (username) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -189,16 +164,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_department_open_pair
|
||||
ON teacher_department_assignments(teacher_id, department_id)
|
||||
WHERE valid_to IS NULL;
|
||||
|
||||
INSERT INTO teacher_department_assignments (teacher_id, department_id, valid_from, is_primary, comment)
|
||||
SELECT u.id, u.department_id, COALESCE(u.created_at::DATE, CURRENT_DATE), TRUE, 'Начальная кафедра из users.department_id'
|
||||
FROM users u
|
||||
WHERE u.role = 'TEACHER'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM teacher_department_assignments existing
|
||||
WHERE existing.teacher_id = u.id
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teacher_creation_requests (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||
@@ -243,12 +208,6 @@ CREATE TABLE IF NOT EXISTS education_forms (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO education_forms (name) VALUES
|
||||
('Бакалавриат'),
|
||||
('Магистратура'),
|
||||
('Специалитет')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Учебные группы
|
||||
-- ==========================================
|
||||
@@ -272,19 +231,6 @@ CREATE TABLE IF NOT EXISTS student_groups (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
-- Тестовая базовая группа для работы
|
||||
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_id, specialty_profile_id, year_start_study)
|
||||
SELECT data.name, data.group_size, ef.id, dept.id, specialty.id, profile.id, data.year_start_study
|
||||
FROM (VALUES
|
||||
('ИВТ-21-1', 25::BIGINT, 'Бакалавриат', 'Кафедра ИБ', '09.03.01', 2025::BIGINT),
|
||||
('ИБ-41м', 15::BIGINT, 'Магистратура', 'Кафедра ИБ', '10.03.01', 2024::BIGINT)
|
||||
) AS data(name, group_size, education_form_name, department_name, specialty_code, year_start_study)
|
||||
JOIN education_forms ef ON ef.name = data.education_form_name
|
||||
JOIN departments dept ON dept.name = data.department_name
|
||||
JOIN specialties specialty ON specialty.specialty_code = data.specialty_code
|
||||
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM student_groups existing WHERE existing.name = data.name);
|
||||
|
||||
-- ==========================================
|
||||
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
|
||||
-- ==========================================
|
||||
@@ -373,22 +319,6 @@ CREATE TRIGGER validate_subgroup_student_capacity
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_subgroup_student_capacity();
|
||||
|
||||
INSERT INTO subgroups (group_id, name, student_capacity)
|
||||
SELECT
|
||||
sg.id,
|
||||
data.name,
|
||||
CASE
|
||||
WHEN data.part_number = 1 THEN CEIL(sg.group_size::NUMERIC / 2)::INT
|
||||
ELSE FLOOR(sg.group_size::NUMERIC / 2)::INT
|
||||
END
|
||||
FROM student_groups sg
|
||||
CROSS JOIN (VALUES
|
||||
('Подгруппа 1', 1),
|
||||
('Подгруппа 2', 2)
|
||||
) AS data(name, part_number)
|
||||
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
|
||||
ON CONFLICT (group_id, name) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Справочники
|
||||
-- ==========================================
|
||||
@@ -414,14 +344,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_subjects_name_ci
|
||||
COMMENT ON INDEX uq_subjects_name_ci IS
|
||||
'Глобально уникальное название дисциплины без учёта регистра';
|
||||
|
||||
INSERT INTO subjects (name, department_id) VALUES
|
||||
('Высшая математика', 1),
|
||||
('Философия', 1),
|
||||
('Информатика', 1),
|
||||
('Базы данных', 1),
|
||||
('Английский язык', 1)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subject_comments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||
@@ -441,12 +363,6 @@ CREATE TABLE IF NOT EXISTS lesson_types (
|
||||
duration_minutes INT DEFAULT 90
|
||||
);
|
||||
|
||||
INSERT INTO lesson_types (name, color_code) VALUES
|
||||
('Лекция', '#FF6B6B'),
|
||||
('Практика', '#4ECDC4'),
|
||||
('Лабораторная работа', '#45B7D1')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Оборудование
|
||||
CREATE TABLE IF NOT EXISTS equipments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -461,15 +377,6 @@ CREATE TABLE IF NOT EXISTS equipments (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO equipments (name) VALUES
|
||||
('Проектор'),
|
||||
('ПК'),
|
||||
('Лаборатория'),
|
||||
('Интерактивная доска'),
|
||||
('Документ-камера'),
|
||||
('Аудиосистема')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Аудитории
|
||||
CREATE TABLE IF NOT EXISTS classrooms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -488,12 +395,6 @@ CREATE TABLE IF NOT EXISTS classrooms (
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO classrooms (name, capacity, building, floor) VALUES
|
||||
('101 Ленинская', 120, 'Главный корпус', 1),
|
||||
('202 IT Lab', 20, 'Корпус IT', 2),
|
||||
('303 Обычная', 30, 'Главный корпус', 3)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Привязка оборудования к аудиториям (Many-to-Many)
|
||||
CREATE TABLE IF NOT EXISTS classroom_equipments (
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
|
||||
@@ -503,20 +404,6 @@ CREATE TABLE IF NOT EXISTS classroom_equipments (
|
||||
PRIMARY KEY (classroom_id, equipment_id)
|
||||
);
|
||||
|
||||
INSERT INTO classroom_equipments (classroom_id, equipment_id, quantity)
|
||||
SELECT c.id, e.id,
|
||||
CASE
|
||||
WHEN e.name = 'ПК' AND c.name = '202 IT Lab' THEN 15
|
||||
WHEN e.name = 'ПК' THEN 1
|
||||
ELSE 1
|
||||
END
|
||||
FROM classrooms c, equipments e
|
||||
WHERE
|
||||
(c.name = '101 Ленинская' AND e.name IN ('Проектор', 'Интерактивная доска', 'Аудиосистема'))
|
||||
OR (c.name = '202 IT Lab' AND e.name IN ('ПК', 'Проектор', 'Лаборатория', 'Интерактивная доска'))
|
||||
OR (c.name = '303 Обычная' AND e.name IN ('Проектор'))
|
||||
ON CONFLICT (classroom_id, equipment_id) DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_status ON departments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_specialties_status ON specialties(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_specialty_profiles_status ON specialty_profiles(status);
|
||||
@@ -575,11 +462,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_time_slot_scopes_weekday
|
||||
ON time_slot_scopes(day_of_week)
|
||||
WHERE apply_mode = 'WEEKDAY';
|
||||
|
||||
INSERT INTO time_slot_scopes (code, name, apply_mode, day_of_week, system_scope, display_order) VALUES
|
||||
('default', 'Базовая сетка', 'DEFAULT', NULL, TRUE, 10),
|
||||
('saturday', 'Субботняя сетка', 'WEEKDAY', 6, TRUE, 20)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS time_slots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time_slot_scope_id BIGINT NOT NULL REFERENCES time_slot_scopes(id) ON DELETE CASCADE,
|
||||
@@ -597,21 +479,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_time_slots_scope_order
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_time_slots_scope ON time_slots(time_slot_scope_id, order_number);
|
||||
|
||||
INSERT INTO time_slots (time_slot_scope_id, order_number, start_time, end_time, duration_minutes)
|
||||
SELECT scope.id, data.order_number, data.start_time::TIME, data.end_time::TIME, data.duration_minutes
|
||||
FROM (VALUES
|
||||
(1, '08:00', '09:30', 90),
|
||||
(2, '09:40', '11:10', 90),
|
||||
(3, '11:40', '13:10', 90),
|
||||
(4, '13:20', '14:50', 90),
|
||||
(5, '15:00', '16:30', 90),
|
||||
(6, '16:50', '18:20', 90),
|
||||
(7, '18:30', '20:00', 90)
|
||||
) AS data(order_number, start_time, end_time, duration_minutes)
|
||||
CROSS JOIN time_slot_scopes scope
|
||||
WHERE scope.code IN ('default', 'saturday')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS time_slot_date_assignments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
assignment_date DATE NOT NULL UNIQUE,
|
||||
@@ -629,11 +496,6 @@ CREATE TABLE IF NOT EXISTS academic_years (
|
||||
CONSTRAINT chk_academic_years_dates CHECK (start_date <= end_date)
|
||||
);
|
||||
|
||||
INSERT INTO academic_years (title, start_date, end_date) VALUES
|
||||
('2024-2025', '2024-09-01', '2025-06-30'),
|
||||
('2025-2026', '2025-09-01', '2026-06-30')
|
||||
ON CONFLICT (title) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS semesters (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||||
@@ -645,16 +507,6 @@ CREATE TABLE IF NOT EXISTS semesters (
|
||||
CONSTRAINT uq_semesters_year_type UNIQUE (academic_year_id, semester_type)
|
||||
);
|
||||
|
||||
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
|
||||
SELECT id, 'autumn', start_date, MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 1, 31)
|
||||
FROM academic_years
|
||||
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
|
||||
|
||||
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
|
||||
SELECT id, 'spring', MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 2, 1), end_date
|
||||
FROM academic_years
|
||||
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
|
||||
@@ -667,22 +519,6 @@ CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
|
||||
description TEXT
|
||||
);
|
||||
|
||||
INSERT INTO academic_calendar_activity_types (code, name, allow_schedule, color_code, display_order, description) VALUES
|
||||
('Т', 'Теоретическое обучение', TRUE, '#10b981', 10, 'Обычные занятия динамического расписания разрешены'),
|
||||
('Э', 'Экзаменационная сессия', FALSE, '#f59e0b', 20, 'Обычные занятия не генерируются'),
|
||||
('К', 'Каникулы', FALSE, '#38bdf8', 30, 'Обычные занятия не генерируются'),
|
||||
('У', 'Учебная практика', FALSE, '#a78bfa', 40, 'Обычные занятия не генерируются'),
|
||||
('П', 'Производственная практика', FALSE, '#fb7185', 50, 'Обычные занятия не генерируются'),
|
||||
('Пд', 'Преддипломная практика', FALSE, '#f97316', 60, 'Обычные занятия не генерируются'),
|
||||
('Н', 'Научно-исследовательская работа', FALSE, '#22c55e', 70, 'Обычные занятия не генерируются'),
|
||||
('Г', 'Подготовка к сдаче и сдача государственного экзамена', FALSE, '#ef4444', 80, 'Обычные занятия не генерируются'),
|
||||
('Д', 'Подготовка к защите и защита выпускной квалификационной работы', FALSE, '#dc2626', 90, 'Обычные занятия не генерируются'),
|
||||
('ПА', 'Повторная промежуточная аттестация', FALSE, '#eab308', 100, 'Обычные занятия не генерируются'),
|
||||
('С', 'Симуляционный курс', FALSE, '#06b6d4', 110, 'Обычные занятия не генерируются'),
|
||||
('*', 'Нерабочий праздничный день', FALSE, '#64748b', 120, 'День исключается из генерации расписания'),
|
||||
('=', 'День вне учебного года', FALSE, '#475569', 130, 'День исключается из генерации расписания')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_calendars (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
@@ -700,21 +536,6 @@ CREATE TABLE IF NOT EXISTS academic_calendars (
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_calendars_context
|
||||
ON academic_calendars(academic_year_id, specialty_id, specialty_profile_id, study_form_id);
|
||||
|
||||
INSERT INTO academic_calendars (title, academic_year_id, specialty_id, specialty_profile_id, study_form_id, course_count)
|
||||
SELECT
|
||||
specialty.specialty_code || ' ' || profile.name || ' ' || study_form.name || ' ' || ay.title,
|
||||
ay.id,
|
||||
specialty.id,
|
||||
profile.id,
|
||||
study_form.id,
|
||||
4
|
||||
FROM academic_years ay
|
||||
CROSS JOIN education_forms study_form
|
||||
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
|
||||
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
|
||||
WHERE ay.title = '2025-2026'
|
||||
ON CONFLICT (academic_year_id, specialty_profile_id, study_form_id, title) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_calendar_periods (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
|
||||
@@ -754,24 +575,6 @@ AS $$
|
||||
) + 1
|
||||
$$;
|
||||
|
||||
INSERT INTO academic_calendar_periods (
|
||||
calendar_id,
|
||||
course_number,
|
||||
start_date,
|
||||
end_date,
|
||||
activity_type_id
|
||||
)
|
||||
SELECT
|
||||
calendar.id,
|
||||
course.course_number,
|
||||
ay.start_date,
|
||||
ay.end_date,
|
||||
activity.id
|
||||
FROM academic_calendars calendar
|
||||
JOIN academic_years ay ON ay.id = calendar.academic_year_id
|
||||
JOIN academic_calendar_activity_types activity ON activity.code = 'Т'
|
||||
CROSS JOIN GENERATE_SERIES(1, calendar.course_count) AS course(course_number);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_calendar_subjects (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
|
||||
@@ -800,21 +603,6 @@ CREATE TABLE IF NOT EXISTS student_group_calendar_assignments (
|
||||
CREATE INDEX IF NOT EXISTS idx_group_calendar_assignments_lookup
|
||||
ON student_group_calendar_assignments(group_id, academic_year_id);
|
||||
|
||||
INSERT INTO student_group_calendar_assignments (group_id, academic_year_id, calendar_id)
|
||||
SELECT
|
||||
sg.id,
|
||||
ay.id,
|
||||
calendar.id
|
||||
FROM student_groups sg
|
||||
JOIN academic_years ay ON ay.title = '2025-2026'
|
||||
JOIN academic_calendars calendar
|
||||
ON calendar.academic_year_id = ay.id
|
||||
AND calendar.specialty_id = sg.specialty_id
|
||||
AND calendar.specialty_profile_id = sg.specialty_profile_id
|
||||
AND calendar.study_form_id = sg.education_form_id
|
||||
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
|
||||
ON CONFLICT (group_id, academic_year_id) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||
@@ -991,121 +779,6 @@ CREATE TRIGGER validate_schedule_rule_slot_subgroups
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_schedule_rule_slot_subgroups();
|
||||
|
||||
CREATE TEMP TABLE tmp_default_rules AS
|
||||
SELECT
|
||||
NEXTVAL(PG_GET_SERIAL_SEQUENCE('schedule_rules', 'id')) AS schedule_rule_id,
|
||||
rule_key,
|
||||
subject_id,
|
||||
semester_id,
|
||||
lecture_academic_hours,
|
||||
laboratory_academic_hours,
|
||||
practice_academic_hours,
|
||||
lecture_start_week,
|
||||
laboratory_start_week,
|
||||
practice_start_week
|
||||
FROM (
|
||||
SELECT
|
||||
data.rule_key,
|
||||
subj.id AS subject_id,
|
||||
sem.id AS semester_id,
|
||||
data.lecture_academic_hours,
|
||||
data.laboratory_academic_hours,
|
||||
data.practice_academic_hours,
|
||||
data.lecture_start_week,
|
||||
data.laboratory_start_week,
|
||||
data.practice_start_week
|
||||
FROM (VALUES
|
||||
('stream_math', 'Высшая математика', '2025-2026', 'spring', 72, 0, 0, 1, 1, 1),
|
||||
('ivt_informatics_lab', 'Информатика', '2025-2026', 'spring', 0, 72, 0, 1, 1, 1),
|
||||
('ivt_databases_practice', 'Базы данных', '2025-2026', 'spring', 0, 0, 72, 1, 1, 1),
|
||||
('ib_philosophy', 'Философия', '2025-2026', 'spring', 54, 0, 0, 1, 1, 1)
|
||||
) AS data(
|
||||
rule_key,
|
||||
subject_name,
|
||||
year_title,
|
||||
semester_type,
|
||||
lecture_academic_hours,
|
||||
laboratory_academic_hours,
|
||||
practice_academic_hours,
|
||||
lecture_start_week,
|
||||
laboratory_start_week,
|
||||
practice_start_week
|
||||
)
|
||||
JOIN subjects subj ON subj.name = data.subject_name
|
||||
JOIN academic_years ay ON ay.title = data.year_title
|
||||
JOIN semesters sem ON sem.academic_year_id = ay.id AND sem.semester_type = data.semester_type
|
||||
) prepared_rules;
|
||||
|
||||
INSERT INTO schedule_rules (
|
||||
id,
|
||||
subject_id,
|
||||
semester_id,
|
||||
lecture_academic_hours,
|
||||
laboratory_academic_hours,
|
||||
practice_academic_hours,
|
||||
lecture_start_week,
|
||||
laboratory_start_week,
|
||||
practice_start_week
|
||||
)
|
||||
SELECT
|
||||
schedule_rule_id,
|
||||
subject_id,
|
||||
semester_id,
|
||||
lecture_academic_hours,
|
||||
laboratory_academic_hours,
|
||||
practice_academic_hours,
|
||||
lecture_start_week,
|
||||
laboratory_start_week,
|
||||
practice_start_week
|
||||
FROM tmp_default_rules;
|
||||
|
||||
INSERT INTO schedule_rule_groups (schedule_rule_id, group_id)
|
||||
SELECT
|
||||
rules.schedule_rule_id,
|
||||
sg.id
|
||||
FROM (VALUES
|
||||
('stream_math', 'ИВТ-21-1'),
|
||||
('stream_math', 'ИБ-41м'),
|
||||
('ivt_informatics_lab', 'ИВТ-21-1'),
|
||||
('ivt_databases_practice', 'ИВТ-21-1'),
|
||||
('ib_philosophy', 'ИБ-41м')
|
||||
) AS data(rule_key, group_name)
|
||||
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
|
||||
JOIN student_groups sg ON sg.name = data.group_name
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO schedule_rule_slots (
|
||||
schedule_rule_id,
|
||||
day_of_week,
|
||||
parity,
|
||||
time_slot_id,
|
||||
teacher_id,
|
||||
classroom_id,
|
||||
lesson_type_id,
|
||||
lesson_format
|
||||
)
|
||||
SELECT
|
||||
rules.schedule_rule_id,
|
||||
data.day_of_week,
|
||||
data.parity,
|
||||
ts.id,
|
||||
teacher.id,
|
||||
classroom.id,
|
||||
lt.id,
|
||||
data.lesson_format
|
||||
FROM (VALUES
|
||||
('stream_math', 1, 'BOTH', 3, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Очно'),
|
||||
('ivt_informatics_lab', 3, 'BOTH', 1, 'Тестовый преподаватель', '202 IT Lab', 'Лабораторная работа', 'Очно'),
|
||||
('ivt_databases_practice', 5, 'EVEN', 5, 'Тестовый преподаватель', '303 Обычная', 'Практика', 'Онлайн'),
|
||||
('ib_philosophy', 2, 'ODD', 2, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Онлайн')
|
||||
) AS data(rule_key, day_of_week, parity, time_slot_order, teacher_username, classroom_name, lesson_type_name, lesson_format)
|
||||
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
|
||||
JOIN time_slot_scopes scope ON scope.code = 'default'
|
||||
JOIN time_slots ts ON ts.time_slot_scope_id = scope.id AND ts.order_number = data.time_slot_order
|
||||
JOIN users teacher ON teacher.username = data.teacher_username
|
||||
JOIN classrooms classroom ON classroom.name = data.classroom_name
|
||||
JOIN lesson_types lt ON lt.name = data.lesson_type_name;
|
||||
|
||||
-- ==========================================
|
||||
-- Функция обновления timestamp
|
||||
-- ==========================================
|
||||
@@ -1371,7 +1044,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23514',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: невалидные точечные изменения CANCEL=%s, MOVE=%s, REPLACE=%s, формат=%s, целевая дата=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: невалидные точечные изменения CANCEL=%s, MOVE=%s, REPLACE=%s, формат=%s, целевая дата=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
cancel_violations,
|
||||
move_violations,
|
||||
replace_violations,
|
||||
@@ -1470,7 +1143,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23514',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: нечётные академические часы правил расписания — лекции=%s, лабораторные=%s, практики=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: нечётные академические часы правил расписания — лекции=%s, лабораторные=%s, практики=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
lecture_violations,
|
||||
laboratory_violations,
|
||||
practice_violations
|
||||
@@ -1511,7 +1184,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23505',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: найдено групп точных дублей слотов правил=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: найдено групп точных дублей слотов правил=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
duplicate_slot_groups
|
||||
);
|
||||
END IF;
|
||||
@@ -1577,7 +1250,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23514',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: неверная длительность слотов=%s, пересекающиеся интервалы=%s, слоты правил вне базовой сетки=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: неверная длительность слотов=%s, пересекающиеся интервалы=%s, слоты правил вне базовой сетки=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
duration_violations,
|
||||
overlap_violations,
|
||||
scope_violations
|
||||
@@ -1733,7 +1406,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23514',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: пересечения учебных годов=%s, семестры вне границ года=%s, пересечения семестров=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: пересечения учебных годов=%s, семестры вне границ года=%s, пересечения семестров=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
academic_year_overlap_violations,
|
||||
semester_bounds_violations,
|
||||
semester_overlap_violations
|
||||
@@ -1864,7 +1537,7 @@ BEGIN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '23514',
|
||||
MESSAGE = format(
|
||||
'Невозможно создать baseline: несовместимые назначения графиков=%s, периоды активности вне измерений=%s, дисциплины вне измерений=%s. Исправьте seed-данные V1 и повторите создание схемы.',
|
||||
'Невозможно создать baseline: несовместимые назначения графиков=%s, периоды активности вне измерений=%s, дисциплины вне измерений=%s. Исправьте исходные данные и повторите создание схемы.',
|
||||
assignment_violations,
|
||||
calendar_period_violations,
|
||||
calendar_subject_violations
|
||||
@@ -2430,33 +2103,9 @@ CREATE UNIQUE INDEX uq_schedule_versions_published_semester
|
||||
CREATE INDEX idx_schedule_versions_semester_history
|
||||
ON schedule_versions(semester_id, version_number DESC);
|
||||
|
||||
INSERT INTO schedule_versions (
|
||||
semester_id,
|
||||
version_number,
|
||||
name,
|
||||
status,
|
||||
created_at,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
semester.id,
|
||||
1,
|
||||
'Опубликованная версия 1',
|
||||
'PUBLISHED',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM semesters semester;
|
||||
|
||||
ALTER TABLE schedule_rules
|
||||
ADD COLUMN schedule_version_id BIGINT;
|
||||
|
||||
UPDATE schedule_rules rule
|
||||
SET schedule_version_id = version.id,
|
||||
version_group_id = COALESCE(rule.version_group_id, rule.id)
|
||||
FROM schedule_versions version
|
||||
WHERE version.semester_id = rule.semester_id
|
||||
AND version.status = 'PUBLISHED';
|
||||
|
||||
ALTER TABLE schedule_rules
|
||||
ALTER COLUMN schedule_version_id SET NOT NULL,
|
||||
ADD CONSTRAINT fk_schedule_rules_version
|
||||
@@ -2487,14 +2136,6 @@ CREATE TABLE schedule_version_history (
|
||||
CREATE INDEX idx_schedule_version_history_version
|
||||
ON schedule_version_history(version_id, created_at DESC, id DESC);
|
||||
|
||||
INSERT INTO schedule_version_history (version_id, action, reason, created_at)
|
||||
SELECT id, 'CREATED', 'Начальная версия расписания', created_at
|
||||
FROM schedule_versions;
|
||||
|
||||
INSERT INTO schedule_version_history (version_id, action, reason, created_at)
|
||||
SELECT id, 'PUBLISHED', 'Начальная публикация расписания', published_at
|
||||
FROM schedule_versions;
|
||||
|
||||
COMMENT ON TABLE schedule_versions IS 'Изолированные черновики и опубликованные версии правил расписания';
|
||||
COMMENT ON COLUMN schedule_versions.status IS 'DRAFT, PUBLISHED или ARCHIVED';
|
||||
COMMENT ON COLUMN schedule_versions.based_on_version_id IS 'Версия, из которой был скопирован черновик';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
INSERT INTO academic_calendar_periods (
|
||||
calendar_id,
|
||||
course_number,
|
||||
start_date,
|
||||
end_date,
|
||||
activity_type_id
|
||||
)
|
||||
SELECT
|
||||
calendar.id,
|
||||
course.course_number,
|
||||
academic_year.start_date,
|
||||
academic_year.end_date,
|
||||
theory_activity.id
|
||||
FROM academic_calendars calendar
|
||||
JOIN academic_years academic_year
|
||||
ON academic_year.id = calendar.academic_year_id
|
||||
JOIN academic_calendar_activity_types theory_activity
|
||||
ON theory_activity.code = 'Т'
|
||||
CROSS JOIN LATERAL GENERATE_SERIES(1, calendar.course_count) AS course(course_number)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM academic_calendar_periods existing_period
|
||||
WHERE existing_period.calendar_id = calendar.id
|
||||
);
|
||||
1410
backend/src/main/resources/db/migration/V2__test_data.sql
Normal file
1410
backend/src/main/resources/db/migration/V2__test_data.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,30 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class AcademicCalendarControllerTest {
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.DisplayName("Страница графиков ограничивает размер и сохраняет фильтр учебного года")
|
||||
void returnsBoundedCalendarPage() throws Exception {
|
||||
AcademicCalendarRepository repository = mock(AcademicCalendarRepository.class);
|
||||
var pageable = org.springframework.data.domain.PageRequest.of(0, 100,
|
||||
org.springframework.data.domain.Sort.by("academicYear.startDate").descending()
|
||||
.and(org.springframework.data.domain.Sort.by("title", "id")));
|
||||
when(repository.searchOptions("профиль", 9L, null, null, null, pageable))
|
||||
.thenReturn(new org.springframework.data.domain.PageImpl<>(List.of(), pageable, 0));
|
||||
var controller = new AcademicCalendarController(repository,
|
||||
mock(AcademicCalendarPeriodRepository.class), mock(SubjectRepository.class),
|
||||
mock(AcademicCalendarSubjectRepository.class), mock(AcademicCalendarGridService.class),
|
||||
mock(ScheduleGeneratorService.class), mock(AcademicStructureService.class));
|
||||
org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller).build()
|
||||
.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
.get("/api/admin/academic-calendars/page")
|
||||
.param("query", " ПРОФИЛЬ ").param("academicYearId", "9")
|
||||
.param("page", "-1").param("size", "1000"))
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.size").value(100))
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.page").value(0))
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.items").isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCalendarReturnsCalendarCreatedByTransactionalService() {
|
||||
AcademicCalendarRepository calendarRepository = mock(AcademicCalendarRepository.class);
|
||||
|
||||
@@ -33,6 +33,20 @@ class ScheduleRuleAdminControllerTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.DisplayName("Страница правил принимает фильтры и возвращает метаданные пагинации")
|
||||
void returnsRulesPage() throws Exception {
|
||||
when(service.getPage(7L, "Математика", 2, 25)).thenReturn(
|
||||
new com.magistr.app.dto.PageResponse<>(List.of(), 2, 25, 50, 2, false, true));
|
||||
mockMvc.perform(get("/api/admin/schedule-rules/page")
|
||||
.param("versionId", "7").param("query", "Математика").param("page", "2"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items").isArray())
|
||||
.andExpect(jsonPath("$.page").value(2))
|
||||
.andExpect(jsonPath("$.size").value(25))
|
||||
.andExpect(jsonPath("$.totalItems").value(50));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsBadRequestWithRussianValidationMessage() throws Exception {
|
||||
when(service.create(any(ScheduleRuleDto.class))).thenThrow(new IllegalArgumentException(
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.sql.SQLException;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisplayName("Миграция начальных периодов календарного графика")
|
||||
@DisplayName("Миграция тестовых данных и начальных периодов календарного графика")
|
||||
class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
|
||||
@Container
|
||||
@@ -33,6 +33,7 @@ class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
long emptyCalendarId;
|
||||
long configuredCalendarId;
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
prepareCustomCalendarPrerequisites(connection);
|
||||
emptyCalendarId = insertCalendarFromSeed(connection, "Пустой график для V2", 3);
|
||||
configuredCalendarId = insertCalendarFromSeed(connection, "Настроенный график для V2", 2);
|
||||
insertConfiguredPeriod(connection, configuredCalendarId);
|
||||
@@ -52,6 +53,51 @@ class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("V1 создаёт только схему, а V2 загружает полный тестовый набор")
|
||||
void migrationSeparatesSchemaAndTestData() throws SQLException {
|
||||
Flyway baseline = flyway("1");
|
||||
baseline.clean();
|
||||
baseline.migrate();
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(queryLong(connection, """
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM departments)
|
||||
+ (SELECT COUNT(*) FROM users)
|
||||
+ (SELECT COUNT(*) FROM student_groups)
|
||||
+ (SELECT COUNT(*) FROM subjects)
|
||||
+ (SELECT COUNT(*) FROM academic_years)
|
||||
+ (SELECT COUNT(*) FROM schedule_rules)
|
||||
"""))
|
||||
.as("После V1 доменные таблицы должны быть пустыми")
|
||||
.isZero();
|
||||
}
|
||||
|
||||
Flyway allMigrations = flyway(null);
|
||||
allMigrations.migrate();
|
||||
|
||||
assertThat(allMigrations.info().current()).isNotNull();
|
||||
assertThat(allMigrations.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM departments")).isEqualTo(41);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM specialties")).isEqualTo(42);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM users WHERE role = 'TEACHER'"))
|
||||
.isEqualTo(151);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM student_groups")).isEqualTo(202);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM subgroups")).isEqualTo(404);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM subjects")).isEqualTo(229);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM classrooms")).isEqualTo(80);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM academic_calendars")).isEqualTo(132);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM academic_calendar_periods")).isEqualTo(576);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM student_group_calendar_assignments"))
|
||||
.isEqualTo(202);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM schedule_rules")).isEqualTo(610);
|
||||
assertThat(queryLong(connection, "SELECT COUNT(*) FROM schedule_rule_slots")).isEqualTo(610);
|
||||
}
|
||||
}
|
||||
|
||||
private Flyway flyway(String targetVersion) {
|
||||
var configuration = Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
@@ -75,10 +121,12 @@ class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
study_form_id,
|
||||
course_count
|
||||
)
|
||||
SELECT ?, academic_year_id, specialty_id, specialty_profile_id, study_form_id, ?
|
||||
FROM academic_calendars
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
SELECT ?, academic_year.id, specialty.id, profile.id, study_form.id, ?
|
||||
FROM academic_years academic_year
|
||||
JOIN specialties specialty ON specialty.specialty_code = '99.99.99'
|
||||
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id
|
||||
JOIN education_forms study_form ON study_form.name = 'Тестовая форма'
|
||||
WHERE academic_year.title = '2030-2031'
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, title);
|
||||
@@ -90,6 +138,40 @@ class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCustomCalendarPrerequisites(Connection connection) throws SQLException {
|
||||
try (var statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO departments (name, code)
|
||||
VALUES ('Тестовая кафедра для V2', 9999)
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO specialties (name, specialty_code)
|
||||
VALUES ('Тестовое направление для V2', '99.99.99')
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO specialty_profiles (specialty_id, name)
|
||||
SELECT id, 'Тестовый профиль для V2'
|
||||
FROM specialties
|
||||
WHERE specialty_code = '99.99.99'
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO education_forms (name)
|
||||
VALUES ('Тестовая форма')
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO academic_years (title, start_date, end_date)
|
||||
VALUES ('2030-2031', DATE '2030-09-01', DATE '2031-06-30')
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
INSERT INTO academic_calendar_activity_types (
|
||||
code, name, allow_schedule, color_code, display_order
|
||||
) VALUES
|
||||
('Т', 'Теоретическое обучение', TRUE, '#10b981', 10),
|
||||
('К', 'Каникулы', FALSE, '#38bdf8', 30)
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
private void insertConfiguredPeriod(Connection connection, long calendarId) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_calendar_periods (
|
||||
@@ -140,6 +222,14 @@ class AcademicCalendarDefaultTheoryPeriodMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private long queryLong(Connection connection, String sql) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
}
|
||||
|
||||
private record PeriodSummary(int count,
|
||||
int minCourse,
|
||||
int maxCourse,
|
||||
|
||||
@@ -38,7 +38,7 @@ class AcademicStructureInvariantIntegrationTest {
|
||||
flyway.migrate();
|
||||
|
||||
assertThat(flyway.info().current()).isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
long groupId = queryLong(
|
||||
@@ -81,6 +81,7 @@ class AcademicStructureInvariantIntegrationTest {
|
||||
"23514",
|
||||
"сохранённые назначения календарных графиков станут несовместимыми"
|
||||
);
|
||||
deleteOtherCalendarAssignments(connection, calendarId, assignmentId);
|
||||
assertSqlState(
|
||||
() -> updateInt(
|
||||
connection,
|
||||
@@ -386,7 +387,7 @@ class AcademicStructureInvariantIntegrationTest {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
@@ -541,6 +542,20 @@ class AcademicStructureInvariantIntegrationTest {
|
||||
""");
|
||||
}
|
||||
|
||||
private void deleteOtherCalendarAssignments(Connection connection,
|
||||
long calendarId,
|
||||
long preservedAssignmentId) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
DELETE FROM student_group_calendar_assignments
|
||||
WHERE calendar_id = ?
|
||||
AND id <> ?
|
||||
""")) {
|
||||
statement.setLong(1, calendarId);
|
||||
statement.setLong(2, preservedAssignmentId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLong(Connection connection, String sql, long value, long id) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setLong(1, value);
|
||||
|
||||
@@ -37,8 +37,8 @@ class ScheduleOverrideMigrationIntegrationTest {
|
||||
.as("После полной миграции должна существовать текущая версия")
|
||||
.isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion())
|
||||
.as("Последней успешно применённой миграцией должна быть V1")
|
||||
.isEqualTo("1");
|
||||
.as("Тестовые данные должны быть загружены миграцией V2")
|
||||
.isEqualTo("2");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
SeedIds seedIds = loadSeedIds(connection);
|
||||
@@ -149,7 +149,7 @@ class ScheduleOverrideMigrationIntegrationTest {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ class ScheduleRuleHoursMigrationIntegrationTest {
|
||||
.as("После полной миграции должна существовать текущая версия")
|
||||
.isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion())
|
||||
.as("Последней успешно применённой миграцией должна быть V1")
|
||||
.isEqualTo("1");
|
||||
.as("Тестовые данные должны быть загружены миграцией V2")
|
||||
.isEqualTo("2");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
SeedIds seedIds = loadSeedIds(connection);
|
||||
@@ -82,7 +82,7 @@ class ScheduleRuleHoursMigrationIntegrationTest {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class SubjectOwnershipInvariantIntegrationTest {
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
flyway.clean();
|
||||
|
||||
@@ -180,7 +180,7 @@ class TeacherDepartmentInvariantIntegrationTest {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ class TeacherPreferencesMigrationIntegrationTest {
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion("1"))
|
||||
.target(MigrationVersion.fromVersion("2"))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
flyway.clean();
|
||||
flyway.migrate();
|
||||
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(queryLong(connection, """
|
||||
SELECT count(*)
|
||||
|
||||
@@ -41,7 +41,7 @@ class TimeSlotInvariantMigrationIntegrationTest {
|
||||
flyway.migrate();
|
||||
|
||||
assertThat(flyway.info().current()).isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
long manualScopeId = insertManualScope(connection, "migration_checks");
|
||||
@@ -171,7 +171,7 @@ class TimeSlotInvariantMigrationIntegrationTest {
|
||||
}
|
||||
|
||||
private Flyway baselineFlyway() {
|
||||
return flyway("1");
|
||||
return flyway("2");
|
||||
}
|
||||
|
||||
private Flyway flyway(String targetVersion) {
|
||||
|
||||
@@ -47,6 +47,7 @@ import static org.mockito.Mockito.verify;
|
||||
classes = ScheduleRuleServiceConcurrencyIntegrationTest.TestApplication.class,
|
||||
properties = {
|
||||
"spring.jpa.open-in-view=false",
|
||||
"spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true",
|
||||
"management.endpoint.health.validate-group-membership=false"
|
||||
}
|
||||
)
|
||||
@@ -86,6 +87,48 @@ class ScheduleRuleServiceConcurrencyIntegrationTest {
|
||||
assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.DisplayName("Страницы правил не пересекаются, поиск работает вне первой страницы")
|
||||
void pagesRulesAndSearchesAcrossWholeVersion() {
|
||||
Long versionId = jdbcTemplate.queryForObject("""
|
||||
SELECT schedule_version_id FROM schedule_rules
|
||||
WHERE status <> 'ARCHIVED' GROUP BY schedule_version_id
|
||||
ORDER BY COUNT(*) DESC LIMIT 1
|
||||
""", Long.class);
|
||||
var first = service.getPage(versionId, "", 0, 25);
|
||||
var second = service.getPage(versionId, "", 1, 25);
|
||||
assertThat(first.items()).hasSize(25);
|
||||
assertThat(second.items()).hasSize(25);
|
||||
assertThat(first.totalItems()).isGreaterThan(50);
|
||||
assertThat(first.items()).extracting(ScheduleRuleDto::id)
|
||||
.doesNotContainAnyElementsOf(second.items().stream().map(ScheduleRuleDto::id).toList());
|
||||
var rule = second.items().get(0);
|
||||
assertThat(rule.slots()).isNotEmpty();
|
||||
assertThat(service.getPage(versionId, rule.id().toString(), 0, 25).items())
|
||||
.extracting(ScheduleRuleDto::id).contains(rule.id());
|
||||
assertThat(service.getPage(versionId, rule.subjectName().toUpperCase(java.util.Locale.ROOT), 0, 25).items())
|
||||
.isNotEmpty().allMatch(item -> item.subjectName().equals(rule.subjectName()));
|
||||
assertThat(service.getPage(versionId, rule.groupNames().get(0), 0, 25).items())
|
||||
.isNotEmpty().allMatch(item -> item.groupNames().contains(rule.groupNames().get(0)));
|
||||
assertThat(service.getPage(versionId, "НетТакойДисциплины", 0, 25).totalItems()).isZero();
|
||||
assertThat(service.getPage(versionId, "", 100000, 25).items()).isEmpty();
|
||||
assertThat(service.getPage(versionId, "", -1, 1000).size()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private com.magistr.app.repository.AcademicCalendarRepository calendarRepository;
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.DisplayName("Календарные графики ищутся по году с ограничением страницы в БД")
|
||||
void pagesCalendarsForFutureYears() {
|
||||
var page = calendarRepository.searchOptions("2027-2028", null, null, null, null,
|
||||
org.springframework.data.domain.PageRequest.of(0, 25,
|
||||
org.springframework.data.domain.Sort.by("academicYear.startDate").descending()
|
||||
.and(org.springframework.data.domain.Sort.by("title", "id"))));
|
||||
assertThat(page.getTotalElements()).isEqualTo(44);
|
||||
assertThat(page.getContent()).hasSize(25).allMatch(calendar -> calendar.getAcademicYear().getTitle().equals("2027-2028"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializesConcurrentCreateInsideOneSemesterAcrossSpringTransactions() throws Exception {
|
||||
assertThat(AopUtils.isAopProxy(service)).isTrue();
|
||||
|
||||
Reference in New Issue
Block a user