feat: Enhance group selection logic to exclude duplicates for main schedules and past documents
This commit is contained in:
@@ -24,7 +24,7 @@ Vite работает на http://127.0.0.1:5173 и проксирует `/api`
|
||||
|
||||
- Живой каталог публичного Nextcloud университета: факультет, форма, курс и поиск по названию группы/файла. Проверенный каталог содержит 113 документов Word.
|
||||
- Расписание по дням или на всю неделю, поиск предмета, преподавателя и аудитории.
|
||||
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (`/api/groups`).
|
||||
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (`/api/groups`). Если у группы есть основное расписание, в выборе не показываются её расписание зачётов и экзаменационной сессии (они идут в обратный отсчёт сессии) и уже прошедшие документы с датами, например неделя «Введение в высшее образование». Группе, у которой есть только такой документ, он остаётся доступен.
|
||||
- Общие ячейки попадают в каждую соответствующую группу, чужие занятия отсекаются.
|
||||
- Отдельные карточки для альтернативных занятий и условий по неделям. Сохраняются исходный текст, цвет ячейки, подгруппы, даты и ограничения.
|
||||
- Учебные недели по таблице университета: переключатель недель с датами, показ только занятий выбранной недели, статус занятия («ещё 4 занятия · до 27 нояб.», «начнётся 20 нояб.») и сетка недель в деталях. Режим «Все строки Word» показывает документ как есть.
|
||||
|
||||
@@ -382,8 +382,26 @@ export function examsOf(idx, group, faculty) {
|
||||
export const groupKey = (s) =>
|
||||
(s || "").toLowerCase().replace(/ё/g, "е").replace(/[\s\-–_.]/g, "");
|
||||
|
||||
/** Every group of every document, with the catalog place it came from. */
|
||||
export function groupEntries(idx) {
|
||||
/**
|
||||
* Documents that only add to a group's timetable: their credits, exams and consultations reach
|
||||
* the student through the session countdown (/api/exams), so the group picker offers them only
|
||||
* when the group has nothing else.
|
||||
*/
|
||||
const SUPPLEMENTS = new Set(["Зачёты", "Экзаменационная сессия"]);
|
||||
|
||||
/** The last date of a document whose every lesson is dated ("Введение в высшее образование"), else null. */
|
||||
function lastDate(schedule) {
|
||||
const dates = schedule.lessons.flatMap((l) => (l.date ? [l.date] : l.dates || []));
|
||||
return dates.length && schedule.lessons.every((l) => l.date || l.dates?.length)
|
||||
? dates.sort().at(-1)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every group of every document, with the catalog place it came from. A group with a main
|
||||
* timetable is not offered its supplements, nor dated documents that are already over.
|
||||
*/
|
||||
export function groupEntries(idx, today = todayIso()) {
|
||||
const list = [];
|
||||
for (const { file, schedule } of idx.docs)
|
||||
for (const group of schedule.groups)
|
||||
@@ -397,8 +415,11 @@ export function groupEntries(idx) {
|
||||
form: file.form || "",
|
||||
period: schedule.period || "",
|
||||
modified: file.modified || null,
|
||||
over: (lastDate(schedule) ?? "9999") < today,
|
||||
});
|
||||
return list;
|
||||
const secondary = (e) => SUPPLEMENTS.has(e.period) || e.over;
|
||||
const main = new Set(list.filter((e) => !secondary(e)).map((e) => e.key));
|
||||
return list.filter((e) => !secondary(e) || !main.has(e.key)).map(({ over, ...e }) => e);
|
||||
}
|
||||
|
||||
const count = (items, pick) => {
|
||||
@@ -416,8 +437,8 @@ const count = (items, pick) => {
|
||||
* Groups matching a query and/or catalog filters. The query ignores filters on
|
||||
* purpose: a student typing their group must find it in any faculty.
|
||||
*/
|
||||
export function findGroups(idx, { q = "", faculty = "", course = "", form = "" } = {}) {
|
||||
const all = groupEntries(idx);
|
||||
export function findGroups(idx, { q = "", faculty = "", course = "", form = "", today } = {}) {
|
||||
const all = groupEntries(idx, today);
|
||||
const key = groupKey(q);
|
||||
let results;
|
||||
if (key.length >= 2) {
|
||||
|
||||
@@ -166,3 +166,32 @@ test("Поиск группы: любое написание, без учёта
|
||||
assert.equal(findGroups(idx, {}).results.length, 0);
|
||||
assert.equal(findGroups(idx, {}).total, 4);
|
||||
});
|
||||
|
||||
test("Выбор группы: зачёты не дублируют группу с основным расписанием", async () => {
|
||||
const { findGroups } = await import("../server/directory.js");
|
||||
const credits = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" });
|
||||
const main = { ...credits, period: "Осенний семестр" };
|
||||
const doc = (id, schedule) => ({ id, file: { id, name: "26-АРХ.docx", faculty: "ИСФ", course: "1 курс" }, schedule });
|
||||
// With a semester timetable the credits stay in the session countdown, not in the picker.
|
||||
const both = findGroups({ docs: [doc("main", main), doc("credits", credits)] }, { q: "26-АРХ" });
|
||||
assert.deepEqual(both.results.map((r) => [r.group, r.fileId]), [["26-АРХ", "main"]]);
|
||||
assert.equal(both.total, 1);
|
||||
// A group with only a credit schedule can still open it.
|
||||
const only = findGroups({ docs: [doc("credits", credits)] }, { q: "26-АРХ" });
|
||||
assert.deepEqual(only.results.map((r) => [r.group, r.fileId, r.period]), [["26-АРХ", "credits", "Зачёты"]]);
|
||||
});
|
||||
|
||||
test("Выбор группы: прошедший документ с датами не дублирует основное расписание", async () => {
|
||||
const { findGroups } = await import("../server/directory.js");
|
||||
// A document dated 8–27 June 2026, like an orientation week, beside the group's semester timetable.
|
||||
const dated = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" });
|
||||
const main = { ...dated, period: "Осенний семестр", lessons: dated.lessons.map(({ date, ...l }) => l) };
|
||||
const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule });
|
||||
const idx = { docs: [doc("main", main), doc("dated", dated)] };
|
||||
const ids = (today) => findGroups(idx, { q: "23-ИТзс", today }).results.map((r) => r.fileId);
|
||||
assert.deepEqual(ids("2026-10-19"), ["main"]);
|
||||
// Before it is over, both are offered.
|
||||
assert.deepEqual(ids("2026-06-20").sort(), ["dated", "main"]);
|
||||
// Alone, even a finished one stays: it is all the group has.
|
||||
assert.deepEqual(findGroups({ docs: [doc("dated", dated)] }, { q: "23-ИТзс", today: "2026-10-19" }).results.length, 1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user