Аудитории: близкие типы объединены в фильтре, добавлен пункт «без типа»
В карточке остаётся формулировка из таблицы («комп.класс Linux»), а в фильтре близкие варианты сведены к одной группе: «комп.класс» (комп.класс, комп.класс Linux, компьютерный класс), «кабинет английского» (английский, кабинет английского), «аудитория кафедры», «лекционная», «лекционно-практическая», «аудитория для практических». Добавлен пункт «без типа» — 4 аудитории, где в таблице тип не указан. Бэкенд отдаёт в /rooms-day и /rooms-free дополнительное поле type_group (room_type_group), фронт фильтрует по нему. Тесты: 160.
This commit is contained in:
10
README.md
10
README.md
@@ -413,7 +413,10 @@ tests/
|
|||||||
- Длинные ссылки в карточке переносятся по служебным символам и каждые
|
- Длинные ссылки в карточке переносятся по служебным символам и каждые
|
||||||
24 символа (`<wbr>`), поэтому карточка не вылезает за экран даже в старых
|
24 символа (`<wbr>`), поэтому карточка не вылезает за экран даже в старых
|
||||||
браузерах, где нет `overflow-wrap`.
|
браузерах, где нет `overflow-wrap`.
|
||||||
- На вкладке «Аудитории» есть фильтры: **тип** (фразой, как в таблице),
|
- На вкладке «Аудитории» есть фильтры: **тип** (в карточке — формулировка из
|
||||||
|
таблицы, в фильтре близкие варианты объединены: «комп.класс» включает
|
||||||
|
«комп.класс Linux» и «компьютерный класс», «кабинет английского» —
|
||||||
|
«английский»; есть пункт «без типа»),
|
||||||
**вместимость** («не меньше N мест») и **оснащение** чекбоксами с выбором
|
**вместимость** («не меньше N мест») и **оснащение** чекбоксами с выбором
|
||||||
условия «все выбранные (И)» или «любое из выбранных (ИЛИ)». Выбранное
|
условия «все выбранные (И)» или «любое из выбранных (ИЛИ)». Выбранное
|
||||||
запоминается в браузере, под списком видно «Показано N из M» и пояснения:
|
запоминается в браузере, под списком видно «Показано N из M» и пояснения:
|
||||||
@@ -448,7 +451,10 @@ tests/
|
|||||||
|
|
||||||
В ответах `rooms-day` и `rooms-free` по каждой аудитории, кроме `room` и `info`
|
В ответах `rooms-day` и `rooms-free` по каждой аудитории, кроме `room` и `info`
|
||||||
(описание как в таблице), идут разобранные атрибуты: `type` (тип фразой из
|
(описание как в таблице), идут разобранные атрибуты: `type` (тип фразой из
|
||||||
таблицы: «комп.класс», «кабинет английского», «лекционная»…), `capacity`
|
таблицы: «комп.класс», «кабинет английского», «лекционная»…) и `type_group`
|
||||||
|
(тот же тип, но близкие формулировки сведены к одной — «комп.класс»,
|
||||||
|
«комп.класс Linux» и «компьютерный класс» дают группу «комп.класс»),
|
||||||
|
`capacity`
|
||||||
(мест; для компьютерных классов — рабочие места; `null`, если в таблице нет
|
(мест; для компьютерных классов — рабочие места; `null`, если в таблице нет
|
||||||
данных), `equipment` (список: «ПК», «Проектор», «Интерактивная панель»,
|
данных), `equipment` (список: «ПК», «Проектор», «Интерактивная панель»,
|
||||||
«Телевизор», «Маркер», «Меловая доска») и `available_days` (дни, если в
|
«Телевизор», «Маркер», «Меловая доска») и `available_days` (дни, если в
|
||||||
|
|||||||
@@ -404,6 +404,31 @@ def room_type(text: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def room_type_group(type_text: str) -> str:
|
||||||
|
"""Общий тип для фильтра: близкие формулировки сводим к одной.
|
||||||
|
|
||||||
|
«комп.класс», «комп.класс Linux», «компьютерный класс» -> «комп.класс»;
|
||||||
|
«английский», «кабинет английского» -> «кабинет английского».
|
||||||
|
В карточке при этом остаётся формулировка из таблицы.
|
||||||
|
"""
|
||||||
|
low = (type_text or "").lower()
|
||||||
|
if not low:
|
||||||
|
return ""
|
||||||
|
if "комп" in low and "класс" in low:
|
||||||
|
return "комп.класс"
|
||||||
|
if "англ" in low:
|
||||||
|
return "кабинет английского"
|
||||||
|
if "каф" in low:
|
||||||
|
return "аудитория кафедры"
|
||||||
|
if low.startswith("лекционно"):
|
||||||
|
return "лекционно-практическая"
|
||||||
|
if low.startswith("лекционн"):
|
||||||
|
return "лекционная"
|
||||||
|
if "практическ" in low:
|
||||||
|
return "аудитория для практических"
|
||||||
|
return type_text
|
||||||
|
|
||||||
|
|
||||||
def room_equipment(text: str) -> list[str]:
|
def room_equipment(text: str) -> list[str]:
|
||||||
"""Оснащение тегами; «без проектора» означает, что проектора нет."""
|
"""Оснащение тегами; «без проектора» означает, что проектора нет."""
|
||||||
negated: set[str] = set()
|
negated: set[str] = set()
|
||||||
@@ -447,10 +472,12 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
|
|||||||
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
|
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
|
||||||
)
|
)
|
||||||
label = f"ауд. {number}"
|
label = f"ауд. {number}"
|
||||||
|
room_kind = room_type(text)
|
||||||
info[label] = {
|
info[label] = {
|
||||||
"room": label,
|
"room": label,
|
||||||
"info": text,
|
"info": text,
|
||||||
"type": room_type(text),
|
"type": room_kind,
|
||||||
|
"type_group": room_type_group(room_kind),
|
||||||
"capacity": room_capacity(text),
|
"capacity": room_capacity(text),
|
||||||
"equipment": room_equipment(text),
|
"equipment": room_equipment(text),
|
||||||
"available_days": room_weekdays(text),
|
"available_days": room_weekdays(text),
|
||||||
@@ -465,6 +492,7 @@ def room_payload(label: str, info: dict | None = None) -> dict:
|
|||||||
"room": label,
|
"room": label,
|
||||||
"info": info.get("info", ""),
|
"info": info.get("info", ""),
|
||||||
"type": info.get("type", ""),
|
"type": info.get("type", ""),
|
||||||
|
"type_group": info.get("type_group", ""),
|
||||||
"capacity": info.get("capacity"),
|
"capacity": info.get("capacity"),
|
||||||
"equipment": list(info.get("equipment") or []),
|
"equipment": list(info.get("equipment") or []),
|
||||||
"available_days": info.get("available_days"),
|
"available_days": info.get("available_days"),
|
||||||
|
|||||||
@@ -469,7 +469,12 @@ function roomAttrsText(room) {
|
|||||||
|
|
||||||
function roomMatchesFilter(room) {
|
function roomMatchesFilter(room) {
|
||||||
const filter = roomsState.filter;
|
const filter = roomsState.filter;
|
||||||
if (filter.type && (room.type || "") !== filter.type) return false;
|
const group = room.type_group || "";
|
||||||
|
if (filter.type === "__none__") {
|
||||||
|
if (group) return false; // только аудитории без указанного типа
|
||||||
|
} else if (filter.type && group !== filter.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const min = parseInt(filter.capacity, 10);
|
const min = parseInt(filter.capacity, 10);
|
||||||
if (!isNaN(min) && min > 0 && (room.capacity == null || room.capacity < min)) return false;
|
if (!isNaN(min) && min > 0 && (room.capacity == null || room.capacity < min)) return false;
|
||||||
const wanted = filter.equipment;
|
const wanted = filter.equipment;
|
||||||
@@ -533,7 +538,8 @@ function drawRoomsScreen(status) {
|
|||||||
|
|
||||||
function roomsToolbar() {
|
function roomsToolbar() {
|
||||||
const all = roomsAll();
|
const all = roomsAll();
|
||||||
const types = [...new Set(all.map((r) => r.type).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ru"));
|
// близкие формулировки типов объединены в общий тип (room_type_group)
|
||||||
|
const types = [...new Set(all.map((r) => r.type_group).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ru"));
|
||||||
const filter = roomsState.filter;
|
const filter = roomsState.filter;
|
||||||
const box = document.createElement("div");
|
const box = document.createElement("div");
|
||||||
box.className = "rooms-filters";
|
box.className = "rooms-filters";
|
||||||
@@ -543,7 +549,7 @@ function roomsToolbar() {
|
|||||||
`<button type="button" class="rf-tab${roomsState.mode === key ? " active" : ""}" data-mode="${key}">${label}</button>`
|
`<button type="button" class="rf-tab${roomsState.mode === key ? " active" : ""}" data-mode="${key}">${label}</button>`
|
||||||
).join("")}
|
).join("")}
|
||||||
${roomsState.mode === "free" ? `
|
${roomsState.mode === "free" ? `
|
||||||
<label class="rf-field rf-narrow"><span>Пара</span>
|
<label class="rf-field rf-narrow">
|
||||||
<select id="rf-period">${ROOM_PERIODS.map((p) =>
|
<select id="rf-period">${ROOM_PERIODS.map((p) =>
|
||||||
`<option value="${p}"${roomsState.period === p ? " selected" : ""}>Пара ${p}</option>`).join("")}</select>
|
`<option value="${p}"${roomsState.period === p ? " selected" : ""}>Пара ${p}</option>`).join("")}</select>
|
||||||
</label>` : ""}
|
</label>` : ""}
|
||||||
@@ -553,6 +559,7 @@ function roomsToolbar() {
|
|||||||
<select id="rf-type">
|
<select id="rf-type">
|
||||||
<option value="">все типы</option>
|
<option value="">все типы</option>
|
||||||
${types.map((t) => `<option value="${escapeHTML(t)}"${filter.type === t ? " selected" : ""}>${escapeHTML(t)}</option>`).join("")}
|
${types.map((t) => `<option value="${escapeHTML(t)}"${filter.type === t ? " selected" : ""}>${escapeHTML(t)}</option>`).join("")}
|
||||||
|
<option value="__none__"${filter.type === "__none__" ? " selected" : ""}>без типа</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="rf-field rf-narrow"><span>Мест не меньше</span>
|
<label class="rf-field rf-narrow"><span>Мест не меньше</span>
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ def test_room_payload_shape():
|
|||||||
payload = parsing.room_payload("ауд. 215", {
|
payload = parsing.room_payload("ауд. 215", {
|
||||||
"info": "лекционная; проектор; 96 мест",
|
"info": "лекционная; проектор; 96 мест",
|
||||||
"type": "лекционная",
|
"type": "лекционная",
|
||||||
|
"type_group": "лекционная",
|
||||||
"capacity": 96,
|
"capacity": 96,
|
||||||
"equipment": ["Проектор"],
|
"equipment": ["Проектор"],
|
||||||
"available_days": None,
|
"available_days": None,
|
||||||
@@ -271,7 +272,22 @@ def test_room_payload_shape():
|
|||||||
"room": "ауд. 215",
|
"room": "ауд. 215",
|
||||||
"info": "лекционная; проектор; 96 мест",
|
"info": "лекционная; проектор; 96 мест",
|
||||||
"type": "лекционная",
|
"type": "лекционная",
|
||||||
|
"type_group": "лекционная",
|
||||||
"capacity": 96,
|
"capacity": 96,
|
||||||
"equipment": ["Проектор"],
|
"equipment": ["Проектор"],
|
||||||
"available_days": None,
|
"available_days": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_type_group_merges_similar():
|
||||||
|
"""Близкие формулировки типов сводятся к общему типу для фильтра."""
|
||||||
|
assert parsing.room_type_group("комп.класс") == "комп.класс"
|
||||||
|
assert parsing.room_type_group("комп.класс Linux") == "комп.класс"
|
||||||
|
assert parsing.room_type_group("компьютерный класс") == "комп.класс"
|
||||||
|
assert parsing.room_type_group("английский") == "кабинет английского"
|
||||||
|
assert parsing.room_type_group("кабинет английского") == "кабинет английского"
|
||||||
|
assert parsing.room_type_group("ауд. каф. ДА") == "аудитория кафедры"
|
||||||
|
assert parsing.room_type_group("лекционная") == "лекционная"
|
||||||
|
assert parsing.room_type_group("лекционно-практическая") == "лекционно-практическая"
|
||||||
|
assert parsing.room_type_group("аудитория для практических") == "аудитория для практических"
|
||||||
|
assert parsing.room_type_group("") == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user