Аудитории: фильтры по типу, вместимости и оснащению + режим «свободные на паре»

Бэкенд:
- parse_rooms_info разбирает описание аудитории (формат «тип; оснащение; N мест»)
  в атрибуты: type (фразой как в таблице), capacity (места, а для компьютерных
  классов — рабочие места; null, если данных нет), equipment (ПК, Проектор,
  Интерактивная панель, Телевизор, Маркер, Меловая доска) и available_days
  («402 четверг пятница»); «без проектора» трактуется как отсутствие;
- /rooms-day и /rooms-free отдают атрибуты (room_payload); аудитории с
  ограничением по дням не предлагаются в /rooms-free в остальные дни.

Фронтенд (вкладка «Аудитории»):
- панель фильтров: тип (значения из данных таблицы), «мест не меньше», оснащение
  чекбоксами с выбором условия «все (И)» / «любое (ИЛИ)»;
- счётчик «Показано N из M» и пояснения: сколько аудиторий скрыто из-за
  отсутствия данных о вместимости, сколько доступны только по отдельным дням;
- переключатель «Занятость за день» / «Свободные на паре» (с выбором пары) —
  вместе с фильтрами это «свободная аудитория на 30 человек с проектором в
  3-ю пару»; выбранные фильтры запоминаются в браузере;
- в карточке аудитории видны разобранные атрибуты и пометки из таблицы.

Тесты: разбор всех форматов описаний (включая отрицания и меловую доску),
API атрибутов и учёта доступных дней. Всего 159.
This commit is contained in:
2026-10-10 12:25:16 +03:00
parent 3d5bfe3023
commit 6f16736359
7 changed files with 626 additions and 68 deletions

View File

@@ -263,3 +263,31 @@ def test_cache_without_link():
assert c.get("/groups").status_code == 502
body = c.get("/status").json()
assert "Ссылка" in body["error"]
def test_rooms_day_and_free_include_attributes(monkeypatch):
"""Аудитории отдаются с типом/вместимостью/оснащением и учётом доступных дней."""
from fixtures import fixture_sheets
from xlsx_builder import build_workbook
sheets = fixture_sheets()
rooms = sheets["аудитории"]
rooms.rows[0].append("402")
rooms.rows[1].append("402 четверг пятница")
cache, _, _ = make_cache(monkeypatch, build_workbook(sheets))
with TestClient(main.create_app(cache)) as client:
day = client.get("/rooms-day", params={"day": "понедельник"}).json()
by_label = {r["room"]: r for r in day["rooms"]}
room215 = by_label["ауд. 215"]
assert room215["type"] == "комп.класс"
assert room215["capacity"] == 12
assert room215["equipment"] == ["ПК"]
assert by_label["ауд. 402"]["available_days"] == ["четверг", "пятница"]
free_monday = client.get("/rooms-free", params={"day": "понедельник", "period": 1}).json()
assert not any(r["room"] == "ауд. 402" for r in free_monday)
assert all("capacity" in r and "equipment" in r for r in free_monday)
free_thursday = client.get("/rooms-free", params={"day": "четверг", "period": 1}).json()
assert any(r["room"] == "ауд. 402" for r in free_thursday)

View File

@@ -176,11 +176,20 @@ def test_group_schedule_shape(data):
def test_rooms_info(sheets):
info = parsing.parse_rooms_info(sheets)
assert info == {
"ауд. 215": {"room": "ауд. 215", "info": "комп.класс, 12 мест"},
"ауд. 301": {"room": "ауд. 301", "info": "интерактивная панель, 24 места"},
"ауд. 220": {"room": "ауд. 220", "info": "20 мест"},
}
assert set(info) == {"ауд. 215", "ауд. 301", "ауд. 220"}
assert info["ауд. 215"]["info"] == "комп.класс, 12 мест"
assert info["ауд. 215"]["type"] == "комп.класс"
assert info["ауд. 215"]["capacity"] == 12
assert info["ауд. 215"]["equipment"] == ["ПК"]
assert info["ауд. 301"]["info"] == "интерактивная панель, 24 места"
assert info["ауд. 301"]["capacity"] == 24
assert info["ауд. 301"]["equipment"] == ["Интерактивная панель"]
assert info["ауд. 220"]["info"] == "20 мест"
assert info["ауд. 220"]["type"] == ""
assert info["ауд. 220"]["capacity"] == 20
assert info["ауд. 220"]["equipment"] == []
def test_teachers_full(sheets):
@@ -205,3 +214,64 @@ def test_build_room_schedule(data):
assert set(out["days"]) == {"понедельник", "вторник"}
assert out["days"]["понедельник"]["1"]["ч"][0]["subject"] == "Физика"
assert parsing.build_room_schedule(rooms, "ауд. 999", times) is None
def test_room_attributes_from_descriptions():
"""Атрибуты аудиторий разбираются из описаний вида «тип; оснащение; N мест»."""
from xlsx_builder import SheetSpec
descriptions = {
"201": "комп.класс МО и СТ; без проектора; маркер; 12 мест",
"203": "аудитория для практических; без проектора; меловая доска; 44 места",
"210": "комп.класс; проектор; маркер; 28 мест",
"305": "кабинет английского; телевизор; маркер; 24 места",
"402": "402 четверг пятница",
"405": "пк",
"410": "",
}
spec = SheetSpec(rows=[
[None] * 6 + list(descriptions.keys()),
[None] * 6 + list(descriptions.values()),
])
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
assert info["ауд. 201"]["type"] == "комп.класс"
assert info["ауд. 201"]["capacity"] == 12
# «без проектора» — проектора нет, хотя слово в тексте есть
assert info["ауд. 201"]["equipment"] == ["ПК", "Маркер"]
assert info["ауд. 201"]["available_days"] is None
assert info["ауд. 203"]["type"] == "аудитория для практических"
assert info["ауд. 203"]["equipment"] == ["Меловая доска"]
assert info["ауд. 203"]["capacity"] == 44
assert info["ауд. 210"]["equipment"] == ["ПК", "Проектор", "Маркер"]
assert info["ауд. 305"]["type"] == "кабинет английского"
assert info["ауд. 305"]["equipment"] == ["Телевизор", "Маркер"]
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"]
assert info["ауд. 402"]["capacity"] is None
assert info["ауд. 402"]["type"] == ""
assert info["ауд. 405"]["equipment"] == ["ПК"]
assert info["ауд. 410"]["info"] == ""
assert info["ауд. 410"]["type"] == ""
def test_room_payload_shape():
payload = parsing.room_payload("ауд. 215", {
"info": "лекционная; проектор; 96 мест",
"type": "лекционная",
"capacity": 96,
"equipment": ["Проектор"],
"available_days": None,
})
assert payload == {
"room": "ауд. 215",
"info": "лекционная; проектор; 96 мест",
"type": "лекционная",
"capacity": 96,
"equipment": ["Проектор"],
"available_days": None,
}