Открытый диплом: учебные планы по годам набора и редакциям
Учебный план стал в «Открытом дипломе» самостоятельной сущностью: планы хранятся по программе, форме обучения и году набора, каждая правка сохраняется новой редакцией, контрольные суммы проверяют план при загрузке, а план, который есть только сканом, вводится как разница к похожему. Как это устроено и как проверено на реальных планах.
До сих пор учебный план в «Открытом дипломе» был вспомогательным файлом: его прикладывали к ведомости, программа брала из него зачётные единицы и итоги, и на этом его роль заканчивалась. Теперь план хранится в базе как самостоятельная сущность — по программе, форме обучения и году набора, с историей правок. Решение записано в ADR-0008, а в прототипе появился раздел «Учебные планы».

Почему план нужен в базе
Сверка архива показала две вещи. Первая: план меняется от года к году, но немного. У соседних годов набора зачётные единицы совпали у всех дисциплин — 51 из 51 и 59 из 59 у двух программ бакалавриата, а у магистерского плана двух соседних лет совпали все колонки всех строк. Вторая: формы обучения одного года отличаются сильнее. У заочной магистратуры семестры сдвинуты на один, а контактной работы 122 часа вместо 820; у заочного бакалавриата технологическая практика идёт одной строкой на 9 з.е., а у очного — двумя, на 6 и 3.
Отсюда и задача: видеть, что именно поменялось, и не вводить план заново каждый год. Ручной ввод чисел из плана в сводную таблицу на каждый выпуск — ровно то место, где рождаются ошибки.
Мешает ещё одно обстоятельство: из 43 PDF-планов архива текст есть только у пяти. Остальные — сканы, и их можно только перепечатать.
Программа, план, редакция
Модель данных состоит из трёх уровней:
- программа — код направления подготовки и профиль; профиль сравнивается без учёта регистра и знаков препинания;
- учебный план — программа, форма обучения и год набора; у каждого года свой план, даже если он совпал с прошлогодним;
- редакция — неизменяемый снимок плана.
Повторная загрузка плана с тем же ключом создаёт новую редакцию, только если отличается хоть одна ячейка или шапка; сравнивается SHA-256 шапки и всех строк. Тот же файл, загруженный ещё раз, получает ответ «уже загружен». Сохранённая редакция не меняется никогда: правка сохраняется новой редакцией со ссылкой на исходную. Так выпуск, проверенный по плану, всегда сможет сослаться на ту редакцию, по которой его проверяли.
Строки хранятся так, как они напечатаны в плане: индекс, название, семестры пяти форм контроля, три колонки зачётных единиц и пять колонок часов. Ключ строки — её позиция, а не индекс, потому что индекс не уникален даже внутри одного плана: «Б.3.1.1» стоит и у выпускной работы, и у государственного экзамена.
Структура плана — родитель строки, блок, листья, альтернативы электива, печатное имя практики — не хранится, а вычисляется при чтении. Поэтому старая редакция не зависит от того, как её разбирала старая версия программы: исправление правил разбора меняет отображение, но не данные.

Контрольные суммы
План проверяет сам себя. В каждой строке зачётные единицы равны сумме единиц на экзамены и на занятия, часы — зачётным единицам, умноженным на 36, занятия — контактной работе плюс самостоятельной. В структуре каждая строка равна сумме вложенных строк, а объём программы — сумме блоков 1–3. Неверное число показывается один раз — у строки над ним, а не по всей цепочке родителей.
Загруженный файл сохраняется в любом случае, а число ошибок записывается в редакцию и видно в списке планов. Правку с несходящимися суммами можно сохранить, только если оператор явно это подтвердит.

Проверка на реальных планах нашла одно ложное срабатывание, и оно поучительно. У «Части, формируемой участниками» часы не сходились ровно на 328. Это элективы по физической культуре: часы у них есть, а зачётных единиц нет, и «Планы» не включают их в сумму части. Теперь строка без зачётных единиц в суммы не входит — так, как считает сам источник.
Сравнение с ближайшим планом
Каждая редакция сравнивается с ближайшим планом программы: той же формы за ближайший предыдущий год, а если такого нет — другой формы за тот же год. Элементы сопоставляются по печатному имени, а не по индексу, поэтому перенумерованная дисциплина остаётся той же дисциплиной. Показывается, что добавлено, что удалено, где изменились зачётные единицы и формы контроля, и как изменились итоги.
На реальных планах сравнение подтвердило то, что раньше пришлось бы искать глазами: магистерские планы двух соседних лет совпали целиком, а сравнение очной и заочной формы одного года показало сдвиг семестров на один и аудиторные часы 820 → 122.
План, который есть только сканом
Распознавание сканов — OCR или небольшая модель — рассматривалось и отложено. Дешевле оказался другой путь: «Создать на основе». Оператор берёт ближайший план и правит только отличия, а контрольные суммы подтверждают результат. Если форма обучения другая, часы контактной и самостоятельной работы очищаются: они зависят от формы, а остальные часы следуют из зачётных единиц. Итоговые часы оператор вписывает по документу.

Проверка шла на настоящем случае: заочный план бакалавриата 2021 года есть только сканом. Он создан на основе очного плана того же года: технологическая практика сведена в одну строку на 9 з.е., вторая строка удалена, в итоговую строку вписаны контактные и самостоятельные часы. Суммы сошлись, сравнение показало ровно эту правку, а все 55 элементов совпали со сканом.
Как проверено
Семь реальных планов — пять PDF с текстом и два Excel — загружены через страницы, и у всех суммы сходятся. Единственное оставшееся предупреждение — заголовок модуля без чисел. Excel и PDF одного плана совпали построчно по всем колонкам. Повторная загрузка того же PDF распознана как «уже загружен», а скан отклонён с подсказкой про «Создать на основе». Проверка шла локально, реальные планы в репозиторий не попадают: тесты строят планы в памяти.
Тестов стало 128 вместо 91, и их снова проверили поломками. Тесты поймали три внесённые ошибки: включение строк без зачётных единиц в суммы, поиск родителя по префиксу индекса без точки — тогда второй «Б.3.1.1» оказывался вложенным в первый — и перенос контактных часов в план другой формы.
Одну поломку тесты не заметили, и это верно. Из настроек убирался явный лимит числа полей формы: у Tomcat 10.1.60 он по умолчанию 10 000, а форма плана из 70 строк — около 1200 полей. Настройка всё равно оставлена: если обновление Tomcat принесёт меньшее значение по умолчанию, длинный план не должен молча обрезаться. Это страхует отдельный тест с формой из 1500 с лишним полей.
Сервер и контейнер
Прототип по-прежнему запускается одним файлом на рабочем месте, но теперь его можно поставить и на сервер. Новая настройка context-path позволяет опубликовать программу под префиксом за обратным прокси, например по адресу https://example.org/open-diplom/: ссылки страниц отсчитываются от <base> с префиксом, перенаправления начинаются с него, а маршруты его не видят. По умолчанию префикс пуст, и настольный запуск не меняется.
Для серверов, где уже работает Docker, есть образ. Его сборка прогоняет все тесты, включая печать и чтение PDF-планов: для них в образ ставится шрифт Liberation Serif с метриками Times New Roman. Сама программа работает в контейнере от непривилегированного пользователя и хранит базу в томе /data.
Дальше
Следующий этап — выпуск: привязка выпуска к конкретной редакции плана и сопоставление строк ведомости с элементами плана в промежуточной зоне. Связи «строка ведомости → элемент плана» будут храниться на уровне программы, чтобы план следующего года подхватывал их сам. Одно ограничение уже известно: «Создать на основе» переносит семестры как есть, а у заочной формы их нумерация другая. Для приложения к диплому семестры не нужны, но в плане их стоит поправить по документу.
Выводы
- Документ, который меняется от года к году, стоит хранить редакциями, а не перезаписывать: иначе теряется, по какой версии проверен результат.
- Структуру лучше вычислять при чтении, а не хранить: тогда исправление правил разбора не требует переписывать старые данные.
- Контрольные суммы превращают ручной ввод из риска в проверяемую операцию — и сами проверяются на реальных данных: ложное срабатывание на 328 часов нашлось только там.
- Скан не обязательно распознавать. Ввод разницы к похожему плану с проверкой сумм дешевле и надёжнее.
- Настройка, которую тесты не могут сломать, — не обязательно лишняя: явный лимит полей защищает от будущего обновления, и это закреплено отдельным тестом.
Доступность
Проект открыт под лицензией Apache 2.0. Решение о хранении планов — docs/architecture/adr/0008-curricula-by-editions.md, результаты проверки — docs/prototype.md.
- Исходный код: github.com/nikita-konkin/open-diplom
Теги
Другие обновления по проекту
Открытый диплом: от ведомости до бланка — выпуск, документы, печать и учёт бланков
За неделю прототип «Открытого диплома» прошёл весь путь выпуска: сопоставление ведомости группы с учебным планом, картотека и регистрационные номера, правки поверх файлов, печат...
Открытый диплом: прототип на Java 11 и сверка, в которой XML совпали побайтно
Черновик архитектуры открытой замены «КиберДиплома» получил прототип и собственный репозиторий. Что прототип умеет, какие критерии выбора стека уже измерены, а какие нет, как пе...