From 81b1e6ae358a01ea096fd2d5abf4fa065b46cb28 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Apr 2026 03:01:43 +0400 Subject: [PATCH 1/4] the client and server are working, I can see the schedule for various institutes, groups, teachers --- README.md | 42 +-- app/__init__.py | 0 app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 148 bytes app/__pycache__/models.cpython-313.pyc | Bin 0 -> 1829 bytes app/__pycache__/parser.cpython-313.pyc | Bin 0 -> 10831 bytes app/__pycache__/ssau_client.cpython-313.pyc | Bin 0 -> 8813 bytes app/models.py | 34 ++ app/parser.py | 257 ++++++++++++++ app/ssau_client.py | 162 +++++++++ main.py | 103 ++++++ requirements.txt | Bin 0 -> 98 bytes static/index.html | 84 +++++ static/script.js | 251 ++++++++++++++ static/style.css | 360 ++++++++++++++++++++ 14 files changed, 1252 insertions(+), 41 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/models.cpython-313.pyc create mode 100644 app/__pycache__/parser.cpython-313.pyc create mode 100644 app/__pycache__/ssau_client.cpython-313.pyc create mode 100644 app/models.py create mode 100644 app/parser.py create mode 100644 app/ssau_client.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 static/index.html create mode 100644 static/script.js create mode 100644 static/style.css diff --git a/README.md b/README.md index 163d41b9a..59542afd5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ # Безопасность веб-приложений. Лабораторка №2 -## Схема сдачи - -1. Получить задание -2. Сделать форк данного репозитория -3. Выполнить задание согласно полученному варианту -4. Сделать PR (pull request) в данный репозиторий -6. Исправить замечания после code review -7. Получить approve -8. Прийти на занятие и защитить работу - Что нужно проявить в работе: - умение разработать завершенное целое веб-приложение, с клиентской и серверной частями (допустимы открытые АПИ) - навыки верстки на html в объеме 200-300 тегов @@ -28,34 +18,4 @@ - справочники групп, табличные данные по расписаниям добывать с настоящего сайта на серверной стороне приложения - в клиентскую часть подгружать эти сведения динамически по JSON-API - обеспечить возможность смотреть расписания в разрезе группы или препода -- обеспечить возможность выбора учебной недели (по умолчанию выбирается автоматически) - -## Вариант 2. Аналог Прибывалки для электричек - -Сделать веб-версию Прибывалки, только для электричек - -Какие нужны возможности: -- находить желаемую ЖД-станцию поиском по названию и по карте -- отображать расписания всех проходящих поездов через выбранную станцию -- отображать расписания для поездов между двумя станциями -- работа через АПИ Яндекс.Расписаний https://yandex.ru/dev/rasp/doc/ru/ (доступ получите сами) -- хорошая работа в условиях экрана смартфона -- бонус: функция "любимых остановок" - -## Вариант 3. Прогноз погоды - -Сделать одностраничный сайт с картой, на которой можно выбрать населенный пункт и получить прогноз погоды на несколько дней по нему. - -Какие нужны возможности: - - увидеть на карте точки с населенными пунктами. Координаты населенных пунктов взять из https://tochno.st/datasets/allsettlements - но все 150 тысяч не нужно, выберите 1 тысячу с самым большим населением. - - при нажатии на точку получить всплывающее окошко с графиками изменения температуры, осадков, силы ветра. API для прогнозов возьмите с https://projecteol.ru/ru/ с соблюдением правил. - - графики рисовать каким-нибудь приличным компонентом, например, https://www.chartjs.org/ - - находить населенный пункт по названию - - можете реализовать с собственным серверным компонентом или придумать, как обойтись без него - - - - - - - +- обеспечить возможность выбора учебной недели (по умолчанию выбирается автоматически) \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a59645673f1a73d7d3cca5df60a30d734694bd2 GIT binary patch literal 148 zcmey&%ge<81d-=2WrFC(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl<>XRDad;?$zz zn1IC6oPhlFq{JAP)Z*-t{DPSB)TH9nWL=|}#DapD`1s7c%#!$cy@JYH95%W6DWy57 Xc15f}GeC9}gBTx~85tRin1L(+o|+?( literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c0dbfcc2f4ac697889935ac45c6b15b49f214fd GIT binary patch literal 1829 zcmaJ?J#X7a7(S9Gk)kA9j=!xqbyL`lh7@R%07cPuNtQZpVt@^@Fd!)TsxqNS^^R&P z6z$@n)l$eq0F7Py7y1*rIRXsgfT969d2^v5Yv1=s*^XnBfF8ad$NPAn_c=|fRhz)` z=gXJDHG`0U(3w7k+?f0X#tY&QN4rn9HLh*zT;Ddhv4JuDKHV_Yl~`sAz=vTix^vz|HY z4AnVK5tud`mMq`%`wno0l|Qb*8q$2u*t0#q4`uWdCM`rS5V15fqJ=6UkjEEr$BPI{ z2+Ig72&)LEd}LLtV+@O*9dsS7ccy>~i%kG1rz+yP@|~4xZn!mYkLYSP1AhEQsc=Dj zCVv22Rw^Zp;;FFA+@H#8>Q6;B^)snacj{+S1BvF}XHq-!GpU(w(aA(+xutxh5}9kc zwrjfPZ5^X|rmW;w;pKLl~O%L zUR4D^u9;iva`v5vFHdN;1k7`6$7R zSv(uRjQ1@}AmGtN2mrj#t&ZM1SRd1CX>DaRe^4FM^>fzZ`lxmAWK6H8^Yzi)gU`lv zBb{9y8G|3k^y)b)SKrJpKOEY16j*Ix_-J(NH)Hhifp=KjJffRf`{42q8YNkTdn<_A z>gLFL7Pl?SP`~WjW`Y+Xl6A4_Shf8(TUlD4X1CYp9RFK1_=(2=Cx)hJf0G-B^Zbc<*ZR>;psKmcISn3G>0co114*&oF literal 0 HcmV?d00001 diff --git a/app/__pycache__/parser.cpython-313.pyc b/app/__pycache__/parser.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2234f7c0d46af8a77368d104de68b64ceee4e577 GIT binary patch literal 10831 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 literal 0 HcmV?d00001 diff --git a/app/__pycache__/ssau_client.cpython-313.pyc b/app/__pycache__/ssau_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..352c9aa1b916a749620384944a9ef90b50da0799 GIT binary patch literal 8813 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 Ke-q5*LH{q1#|NeW literal 0 HcmV?d00001 diff --git a/app/models.py b/app/models.py new file mode 100644 index 000000000..43da1ff94 --- /dev/null +++ b/app/models.py @@ -0,0 +1,34 @@ +from typing import List, Optional, Tuple +from dataclasses import dataclass + +@dataclass +class Lesson: + time_start: str + time_end: str + subject: str + lesson_type: str + room: Optional[str] + teachers: List[dict] + groups: List[dict] + subgroup: Optional[str] + comment: Optional[str] + +@dataclass +class DaySchedule: + weekday: str + date: Optional[str] + date_iso: Optional[str] + lessons: List[Lesson] + +@dataclass +class WeekSchedule: + week_number: Optional[int] + week_label: Optional[str] + week_dates: Optional[str] + week_start_date: Optional[str] + week_end_date: Optional[str] + prev_week: Optional[int] + next_week: Optional[int] + entity_name: str + days: List[DaySchedule] + time_slots: List[Tuple[str, str]] \ No newline at end of file diff --git a/app/parser.py b/app/parser.py new file mode 100644 index 000000000..2532e8ec6 --- /dev/null +++ b/app/parser.py @@ -0,0 +1,257 @@ +import re +from datetime import datetime +from bs4 import BeautifulSoup +from typing import List, Tuple, Optional +from .models import Lesson, DaySchedule, WeekSchedule + +def parse_schedule(html: str): + """Парсит HTML с расписанием и возвращает структурированные данные""" + soup = BeautifulSoup(html, "lxml") + + container = soup.select_one("div.container.timetable") + if not container: + container = soup.find("div", class_=re.compile(r"\btimetable\b")) + if not container: + raise ValueError("Не найден контейнер с расписанием") + + entity_name = "" + h2 = container.find("h2") + if h2: + entity_name = h2.get_text(" ", strip=True) + if not entity_name: + h1 = container.find("h1") + if h1: + text = h1.get_text(" ", strip=True) + if "," in text: + entity_name = text.split(",", 1)[-1].strip() + else: + entity_name = text.strip() + if not entity_name: + entity_name = "—" + + week_label = None + week_elem = container.select_one(".week-nav-current_week") + if week_elem: + week_label = week_elem.get_text(" ", strip=True) + + week_number = None + if week_label: + match = re.search(r"(\d+)", week_label) + if match: + week_number = int(match.group(1)) + + prev_week = None + next_week = None + prev_link = container.select_one(".week-nav-prev") + if prev_link: + href = prev_link.get("href", "") + match = re.search(r"selectedWeek=(\d+)", href) + if match: + prev_week = int(match.group(1)) + + next_link = container.select_one(".week-nav-next") + if next_link: + href = next_link.get("href", "") + match = re.search(r"selectedWeek=(\d+)", href) + if match: + next_week = int(match.group(1)) + + schedule_grid = container.select_one(".schedule .schedule__items") or \ + container.select_one(".schedule__items") + if not schedule_grid: + raise ValueError("Не найден блок schedule__items") + + rows = schedule_grid.find_all(recursive=False) + if not rows: + raise ValueError("Пустой блок расписания") + + headers = [] + row_idx = 0 + while row_idx < len(rows): + row_class = rows[row_idx].get("class") or [] + if "schedule__head" in row_class: + headers.append(rows[row_idx]) + row_idx += 1 + else: + break + + if len(headers) < 2: + raise ValueError("Не удалось найти заголовки дней недели") + + day_headers = headers[1:] + + days: List[DaySchedule] = [] + for header in day_headers: + header_text = header.get_text(" ", strip=True) + + date_match = re.search(r"(\d{2}\.\d{2}\.\d{4})", header_text) + date_str = date_match.group(1) if date_match else None + + dt = None + if date_str: + try: + dt = datetime.strptime(date_str.strip(), "%d.%m.%Y") + except ValueError: + pass + + weekday = header_text.replace(date_str, "").strip() if date_str else header_text.strip() + weekday = re.sub(r"\s+", " ", weekday) or "—" + + days.append(DaySchedule( + weekday=weekday, + date=date_str, + date_iso=dt.date().isoformat() if dt else None, + lessons=[] + )) + + time_slots: List[Tuple[str, str]] = [] + + while row_idx < len(rows): + time_row = rows[row_idx] + row_idx += 1 + + if "schedule__time" not in (time_row.get("class") or []): + continue + + time_items = time_row.select(".schedule__time-item") + times = [item.get_text(" ", strip=True) for item in time_items] + times = [t for t in times if re.search(r"\d{1,2}:\d{2}", t)] + + time_start = times[0] if len(times) >= 2 else "" + time_end = times[1] if len(times) >= 2 else "" + + if time_start and time_end: + if not time_slots or time_slots[-1] != (time_start, time_end): + time_slots.append((time_start, time_end)) + + for day_index in range(len(days)): + if row_idx >= len(rows): + break + cell = rows[row_idx] + row_idx += 1 + + lessons_in_cell = cell.find_all("div", class_="schedule__lesson", recursive=False) + + for lesson_elem in lessons_in_cell: + lesson_type = lesson_elem.select_one(".schedule__lesson-type-chip") + if not lesson_type: + lesson_type = lesson_elem.select_one(".schedule__lesson-type") + lesson_type_text = lesson_type.get_text(" ", strip=True) if lesson_type else "—" + + subject_elem = lesson_elem.select_one(".schedule__discipline") + if not subject_elem: + subject_elem = lesson_elem.select_one(".schedule__discipline-name") + subject = subject_elem.get_text(" ", strip=True) if subject_elem else "—" + + room_elem = lesson_elem.select_one(".schedule__place") + room = room_elem.get_text(" ", strip=True) if room_elem else None + + teachers = [] + teacher_block = lesson_elem.select_one(".schedule__teacher") + if teacher_block: + for link in teacher_block.select('a[href*="staffId="]'): + name = link.get_text(" ", strip=True) + staff_id = None + match = re.search(r"staffId=(\d+)", link.get("href", "")) + if match: + staff_id = int(match.group(1)) + if name: + teachers.append({"staff_id": staff_id, "name": name}) + + groups = [] + groups_block = lesson_elem.select_one(".schedule__groups") + if groups_block: + for link in groups_block.select('a[href*="groupId="]'): + name = link.get_text(" ", strip=True) + group_id = None + match = re.search(r"groupId=(\d+)", link.get("href", "")) + if match: + group_id = int(match.group(1)) + if name: + groups.append({"group_id": group_id, "name": name}) + + subgroup = None + for span in lesson_elem.select("span.caption-text"): + text = span.get_text(" ", strip=True) + if text and "подгрупп" in text.lower(): + match = re.search(r":\s*(.+)$", text) + if match: + subgroup = match.group(1).strip() + else: + parts = text.split() + if parts: + subgroup = parts[-1].strip() + + comment = None + comment_elem = lesson_elem.select_one(".schedule__comment") + if comment_elem: + comment = comment_elem.get_text(" ", strip=True) + if comment and "подгрупп" in comment.lower(): + comment = None + + lesson = Lesson( + time_start=time_start, + time_end=time_end, + subject=subject, + lesson_type=lesson_type_text, + room=room, + teachers=teachers, + groups=groups, + subgroup=subgroup, + comment=comment + ) + days[day_index].lessons.append(lesson) + + valid_dates = [] + for d in days: + if d.date: + try: + dt = datetime.strptime(d.date, "%d.%m.%Y") + valid_dates.append(dt) + except ValueError: + pass + + week_dates = None + week_start = None + week_end = None + + if valid_dates: + min_date = min(valid_dates) + max_date = max(valid_dates) + week_start = min_date.date().isoformat() + week_end = max_date.date().isoformat() + week_dates = f"{min_date.strftime('%d.%m.%Y')} - {max_date.strftime('%d.%m.%Y')}" + + return { + "week_number": week_number, + "week_label": week_label or (f"{week_number} неделя" if week_number is not None else None), + "week_dates": week_dates, + "week_start_date": week_start, + "week_end_date": week_end, + "prev_week": prev_week, + "next_week": next_week, + "entity_name": entity_name, + "days": [ + { + "weekday": day.weekday, + "date": day.date, + "date_iso": day.date_iso, + "lessons": [ + { + "time_start": lesson.time_start, + "time_end": lesson.time_end, + "subject": lesson.subject, + "lesson_type": lesson.lesson_type, + "room": lesson.room, + "teachers": lesson.teachers, + "groups": lesson.groups, + "subgroup": lesson.subgroup, + "comment": lesson.comment + } + for lesson in day.lessons + ] + } + for day in days + ], + "time_slots": [{"time_start": ts[0], "time_end": ts[1]} for ts in time_slots] + } \ No newline at end of file diff --git a/app/ssau_client.py b/app/ssau_client.py new file mode 100644 index 000000000..babda614b --- /dev/null +++ b/app/ssau_client.py @@ -0,0 +1,162 @@ +import re +import requests +from typing import List, Tuple, Optional, Dict +from urllib.parse import urljoin, urlparse, parse_qs +from bs4 import BeautifulSoup + +SSAU_BASE = "https://ssau.ru" + +class SsauClient: + def __init__(self, timeout_seconds: float = 25.0): + self._session = requests.Session() + self._session.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Accept": "text/html,application/json;q=0.9,*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.5", + }) + self._timeout = timeout_seconds + self._cached_csrf: Optional[str] = None + + def fetch_html(self, url: str) -> str: + response = self._session.get(url, timeout=self._timeout) + response.raise_for_status() + return response.text + + def _ensure_csrf_token(self) -> str: + if self._cached_csrf: + return self._cached_csrf + + html = self.fetch_html(urljoin(SSAU_BASE, "/rasp")) + + match = re.search(r'name="csrf-token"\s+content="([^"]+)"', html, re.IGNORECASE) + if not match: + match = re.search(r"name='csrf-token'\s+content='([^']+)'", html, re.IGNORECASE) + + if not match: + raise RuntimeError("Не удалось найти csrf-token на странице /rasp") + + self._cached_csrf = match.group(1) + return self._cached_csrf + + def get_institutes(self) -> List[Tuple[int, str]]: + """Получает список институтов/факультетов""" + html = self.fetch_html(urljoin(SSAU_BASE, "/rasp")) + soup = BeautifulSoup(html, "lxml") + + links = soup.select('a[href*="/rasp/faculty/"]') + institutes: Dict[int, Tuple[int, str]] = {} + + for a in links: + href = a.get("href") + if not href: + continue + + abs_url = urljoin(SSAU_BASE, href) + match = re.search(r"/rasp/faculty/(\d+)", abs_url) + if not match: + continue + + faculty_id = int(match.group(1)) + name = a.get_text(" ", strip=True) + + if name and faculty_id not in institutes: + institutes[faculty_id] = (faculty_id, name) + + return sorted(institutes.values(), key=lambda x: x[1].lower()) + + def get_available_courses(self, faculty_id: int) -> List[int]: + """Получает доступные курсы для института""" + url = urljoin(SSAU_BASE, f"/rasp/faculty/{faculty_id}?course=1") + html = self.fetch_html(url) + soup = BeautifulSoup(html, "lxml") + + courses: set = set() + + for a in soup.select('a[href*="course="]'): + href = a.get("href") or "" + try: + parsed = urlparse(urljoin(SSAU_BASE, href)) + params = parse_qs(parsed.query) + if "course" in params: + course_num = int(params["course"][0]) + if 1 <= course_num <= 5: + courses.add(course_num) + except (ValueError, TypeError): + continue + + if not courses: + courses = {1, 2, 3, 4, 5} + + return sorted(courses) + + def get_groups_by_course(self, faculty_id: int, course: int) -> List[Tuple[int, str]]: + """Получает группы для института и курса""" + url = urljoin(SSAU_BASE, f"/rasp/faculty/{faculty_id}?course={course}") + html = self.fetch_html(url) + soup = BeautifulSoup(html, "lxml") + + groups: Dict[int, Tuple[int, str]] = {} + + for a in soup.select('a[href*="groupId="]'): + href = a.get("href") + if not href: + continue + + abs_url = urljoin(SSAU_BASE, href) + parsed = urlparse(abs_url) + params = parse_qs(parsed.query) + + if "groupId" not in params: + continue + + try: + group_id = int(params["groupId"][0]) + name = a.get_text(" ", strip=True) + if name and group_id not in groups: + groups[group_id] = (group_id, name) + except (ValueError, TypeError): + continue + + return sorted(groups.values(), key=lambda x: x[1]) + + def search_teachers(self, query: str) -> List[dict]: + """Ищет преподавателей по запросу""" + csrf = self._ensure_csrf_token() + + response = self._session.post( + urljoin(SSAU_BASE, "/rasp/search"), + data={"text": query}, + headers={ + "X-CSRF-TOKEN": csrf, + "Content-Type": "application/x-www-form-urlencoded", + "X-Requested-With": "XMLHttpRequest" + }, + timeout=self._timeout + ) + response.raise_for_status() + + data = response.json() + teachers = [] + + for item in data: + if "staffId=" in item.get("url", ""): + teachers.append({ + "id": item.get("id"), + "name": item.get("text", "") + }) + + return teachers + + def get_schedule_html(self, group_id: int, week: int = 0) -> str: + """Получает HTML страницы расписания группы""" + url = urljoin(SSAU_BASE, f"/rasp?groupId={group_id}") + if week != 0: + url += f"&selectedWeek={week}" + return self.fetch_html(url) + + def get_teacher_schedule_html(self, staff_id: int, week: int = 0) -> str: + """Получает HTML страницы расписания преподавателя""" + url = urljoin(SSAU_BASE, f"/rasp?staffId={staff_id}") + if week != 0: + url += f"&selectedWeek={week}" + return self.fetch_html(url) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 000000000..9555160c8 --- /dev/null +++ b/main.py @@ -0,0 +1,103 @@ +from fastapi import FastAPI, Query, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import RedirectResponse +import uvicorn +import os + +from app.ssau_client import SsauClient +from app.parser import parse_schedule + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +client = SsauClient() + +@app.get("/api/institutes") +async def get_institutes(): + try: + institutes = client.get_institutes() + return {"institutes": [{"id": inst_id, "name": name} for inst_id, name in institutes]} + except Exception as e: + print(f"Ошибка: {e}") + return {"institutes": []} + +@app.get("/api/groups") +async def get_groups( + institute_id: int = Query(None), + course: int = Query(None) +): + if course is None: + try: + courses = client.get_available_courses(institute_id) + return {"available_courses": courses} + except Exception as e: + print(f"Ошибка получения курсов: {e}") + return {"available_courses": [1, 2, 3, 4, 5]} + else: + try: + groups = client.get_groups_by_course(institute_id, course) + return {"groups": [{"id": group_id, "name": name} for group_id, name in groups]} + except Exception as e: + print(f"Ошибка получения групп: {e}") + return {"groups": []} + +@app.get("/api/teachers") +async def search_teachers(q: str = Query("", min_length=2)): + if len(q) < 2: + return {"teachers": []} + + try: + teachers = client.search_teachers(q) + return {"teachers": teachers} + except Exception as e: + print(f"Ошибка поиска: {e}") + return {"teachers": []} + +@app.get("/api/schedule/group") +async def get_schedule_group( + group_id: int = Query(..., description="ID группы"), + week: int = Query(0, description="Номер недели (0 - текущая)") +): + try: + html = client.get_schedule_html(group_id, week) + schedule = parse_schedule(html) + return schedule + except Exception as e: + print(f"Ошибка получения расписания: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/schedule/teacher") +async def get_schedule_teacher( + staff_id: int = Query(..., description="ID преподавателя"), + week: int = Query(0, description="Номер недели (0 - текущая)") +): + try: + html = client.get_teacher_schedule_html(staff_id, week) + schedule = parse_schedule(html) + return schedule + except Exception as e: + print(f"Ошибка получения расписания: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +STATIC_DIR = os.path.join(BASE_DIR, "static") + +if not os.path.exists(STATIC_DIR): + os.makedirs(STATIC_DIR) + +app.mount("/static", StaticFiles(directory=STATIC_DIR, html=True), name="static") + +@app.get("/") +async def root(): + return RedirectResponse(url="/static/index.html") + +if __name__ == "__main__": + print("Сервер запущен: http://127.0.0.1:8000") + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..44993fc8481e66f0840e4329e98a6f9a9efdc04b GIT binary patch literal 98 zcmW-XOA3H66a!~1cpEQNrG-MZUyG+#!`tLD$@c~vogpnmRu*oi(m|$i8EJT`xVL9F VwyQa@p>L2UZCPH}Y)?F#qCXJG6FmR` literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html new file mode 100644 index 000000000..128a8720c --- /dev/null +++ b/static/index.html @@ -0,0 +1,84 @@ + + + + + + Расписание + + + +
+
+
+
Расписание занятий
+
Самарский университет
+
+
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + + + + +
Выберите институт, курс и группу
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 000000000..19ab74146 --- /dev/null +++ b/static/script.js @@ -0,0 +1,251 @@ +$(document).ready(function() { + let currentMode = 'group'; + let currentGroupId = null; + let currentTeacherId = null; + let currentSchedule = null; + let currentWeek = null; + + function loadInstitutes() { + $.ajax({ + url: '/api/institutes', + method: 'GET', + success: function(data) { + let $select = $('#institute-select'); + $select.empty().append(''); + $.each(data.institutes || [], function(i, inst) { + $select.append(''); + }); + }, + error: function() { + $('#status').text('Ошибка загрузки институтов').addClass('status--error'); + } + }); + } + + $('#institute-select').on('change', function() { + let instituteId = $(this).val(); + if (!instituteId) return; + + $('#course-select').prop('disabled', true).html(''); + $('#group-select').prop('disabled', true).html(''); + + $.ajax({ + url: '/api/groups', + data: { institute_id: instituteId }, + success: function(data) { + let $select = $('#course-select'); + $select.empty().append(''); + $.each(data.available_courses || [], function(i, course) { + $select.append(''); + }); + $select.prop('disabled', false); + } + }); + }); + + $('#course-select').on('change', function() { + let instituteId = $('#institute-select').val(); + let course = $(this).val(); + if (!instituteId || !course) return; + + $('#group-select').prop('disabled', true).html(''); + + $.ajax({ + url: '/api/groups', + data: { institute_id: instituteId, course: course }, + success: function(data) { + let $select = $('#group-select'); + $select.empty().append(''); + $.each(data.groups || [], function(i, group) { + $select.append(''); + }); + $select.prop('disabled', false); + } + }); + }); + + $('#group-select').on('change', function() { + currentGroupId = $(this).val(); + $('#show-group-btn').prop('disabled', !currentGroupId); + }); + + $('#show-group-btn').on('click', function() { + if (currentGroupId) loadSchedule('group', currentGroupId); + }); + + let searchTimeout; + $('#teacher-input').on('input', function() { + let query = $(this).val().trim(); + clearTimeout(searchTimeout); + + if (query.length < 2) { + $('#teacher-suggestions').hide(); + $('#show-teacher-btn').prop('disabled', true); + return; + } + + searchTimeout = setTimeout(function() { + $.ajax({ + url: '/api/teachers', + data: { q: query }, + success: function(data) { + let $suggestions = $('#teacher-suggestions').empty(); + $.each(data.teachers || [], function(i, teacher) { + $suggestions.append('
' + teacher.name + '
'); + }); + $suggestions.show(); + } + }); + }, 300); + }); + + $(document).on('click', '#teacher-suggestions div', function() { + currentTeacherId = $(this).data('id'); + $('#teacher-input').val($(this).text()); + $('#teacher-suggestions').hide(); + $('#show-teacher-btn').prop('disabled', false); + }); + + $('#show-teacher-btn').on('click', function() { + if (currentTeacherId) loadSchedule('teacher', currentTeacherId); + }); + + function loadSchedule(mode, id, week) { + let url = mode === 'group' ? '/api/schedule/group' : '/api/schedule/teacher'; + let params = mode === 'group' ? { group_id: id } : { staff_id: id }; + if (week) params.week = week; + + currentWeek = week; + $('#status').text('Загрузка...').removeClass('status--error').addClass('status--loading'); + $('#schedule-container').empty(); + + $.ajax({ + url: url, + data: params, + success: function(data) { + currentSchedule = data; + $('#status').text('Расписание загружено').removeClass('status--loading').addClass('status--idle'); + updateWeekNav(data); + renderTable(data); + }, + error: function() { + $('#status').text('Ошибка загрузки').removeClass('status--loading').addClass('status--error'); + } + }); + } + + function updateWeekNav(schedule) { + let weekText = schedule.week_label || (schedule.week_number ? schedule.week_number + ' неделя' : '—'); + $('#week-info').html(weekText); + $('#prev-week-btn').prop('disabled', !schedule.prev_week); + $('#next-week-btn').prop('disabled', !schedule.next_week); + $('#week-navigation').show(); + } + + $('#prev-week-btn').click(function() { + if (currentSchedule?.prev_week) { + if (currentMode === 'group') loadSchedule('group', currentGroupId, currentSchedule.prev_week); + else loadSchedule('teacher', currentTeacherId, currentSchedule.prev_week); + } + }); + + $('#next-week-btn').click(function() { + if (currentSchedule?.next_week) { + if (currentMode === 'group') loadSchedule('group', currentGroupId, currentSchedule.next_week); + else loadSchedule('teacher', currentTeacherId, currentSchedule.next_week); + } + }); + + function renderTable(schedule) { + let days = schedule.days || []; + let timeSlots = schedule.time_slots || []; + + if (!days.length) { + $('#schedule-container').html('
Нет данных
'); + return; + } + + let html = ''; + $.each(days, function(i, day) { + html += ''; + }); + html += ''; + + $.each(timeSlots, function(i, slot) { + let timeStr = slot.time_start + '–' + slot.time_end; + html += ''; + + $.each(days, function(j, day) { + let lessons = $.grep(day.lessons, function(l) { + return l.time_start === slot.time_start; + }); + + if (lessons.length) { + html += ''; + } else { + html += ''; + } + }); + html += ''; + }); + + html += '
Время' + day.weekday + (day.date ? '
' + day.date + '' : '') + '
' + timeStr + ''; + $.each(lessons, function(k, lesson) { + html += '
'; + html += '
' + escapeHtml(lesson.subject) + '
'; + if (lesson.lesson_type) html += '' + escapeHtml(lesson.lesson_type) + ''; + if (lesson.room) html += '
ауд. ' + escapeHtml(lesson.room) + '
'; + if (lesson.teachers && lesson.teachers.length) { + html += '
' + escapeHtml(lesson.teachers[0].name) + '
'; + } + html += '
'; + }); + html += '
'; + $('#schedule-container').html(html); + } + + function escapeHtml(str) { + if (!str) return ''; + return str.replace(/[&<>]/g, function(m) { + if (m === '&') return '&'; + if (m === '<') return '<'; + if (m === '>') return '>'; + return m; + }); + } + + $('.mode-btn').click(function() { + $('.mode-btn').removeClass('mode-btn--active'); + $(this).addClass('mode-btn--active'); + currentMode = $(this).data('mode'); + + if (currentMode === 'group') { + $('#group-mode').show(); + $('#teacher-mode').hide(); + } else { + $('#group-mode').hide(); + $('#teacher-mode').show(); + } + $('#week-navigation').hide(); + $('#schedule-container').empty(); + }); + + $(document).on('click', '.lesson-teacher', function() { + let name = $(this).text(); + $('#teacher-input').val(name); + $('.mode-btn[data-mode="teacher"]').click(); + $('#teacher-suggestions').hide(); + $.ajax({ + url: '/api/teachers', + data: { q: name }, + success: function(data) { + if (data.teachers && data.teachers.length) { + currentTeacherId = data.teachers[0].id; + $('#show-teacher-btn').prop('disabled', false); + } + } + }); + }); + + loadInstitutes(); +}); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 000000000..92001f555 --- /dev/null +++ b/static/style.css @@ -0,0 +1,360 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Arial, sans-serif; + background: #eef2f3; + color: #333; + padding: 15px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 8px; + border: 1px solid #ddd; + overflow: hidden; +} + +.header { + background: #2780E3; + color: white; + padding: 20px; + text-align: center; +} + +.header__inner { + max-width: 1200px; + margin: 0 auto; +} + +.header__title { + font-size: 20px; + font-weight: normal; + margin-bottom: 5px; +} + +.header__subtitle { + font-size: 14px; + opacity: 0.9; +} + +.mode-switch { + display: flex; + gap: 5px; + margin: 15px 20px; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; +} + +.mode-btn { + background: none; + border: none; + padding: 6px 15px; + font-size: 14px; + cursor: pointer; + color: #666; + border-radius: 4px; +} + +.mode-btn:hover { + background: #e8e8e8; +} + +.mode-btn--active { + background: #2780E3; + color: white; +} + +.search-panel { + background: #f9f9f9; + border: 1px solid #ddd; + border-radius: 6px; + padding: 15px; + margin: 0 20px 20px; +} + +.form-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: flex-end; +} + +.form-group { + flex: 1; + min-width: 150px; +} + +.form-group--button { + flex: 0; +} + +.form-label { + display: block; + font-size: 12px; + color: #555; + margin-bottom: 4px; +} + +.form-control { + width: 100%; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 13px; +} + +.form-control:focus { + outline: none; + border-color: #2780E3; +} + +.btn { + padding: 6px 20px; + background: #2780E3; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; +} + +.btn:hover:not(:disabled) { + background: #1a66c4; +} + +.btn:disabled { + background: #aaa; + cursor: not-allowed; +} + +.search-wrapper { + position: relative; +} + +.suggestions-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; +} + +.suggestions-dropdown div { + padding: 6px 10px; + cursor: pointer; + border-bottom: 1px solid #eee; +} + +.suggestions-dropdown div:hover { + background: #e8f0f8; +} + +.week-nav { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + margin: 0 20px 20px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 6px; + background: #f9f9f9; +} + +.week-nav__btn { + padding: 4px 12px; + background: #e0e0e0; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; +} + +.week-nav__btn:hover:not(:disabled) { + background: #d0d0d0; +} + +.week-nav__btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.week-nav__info { + font-size: 14px; + font-weight: bold; + color: #2780E3; +} + +.status { + padding: 8px 15px; + margin: 0 20px 20px; + background: #e8f0f8; + border-radius: 4px; + text-align: center; + font-size: 13px; + color: #2780E3; +} + +.status--error { + background: #ffebee; + color: #c62828; +} + +.schedule-container { + margin: 0 20px 20px; + overflow-x: auto; +} + +.schedule-table { + width: 100%; + border-collapse: collapse; + border: 1px solid #ddd; + border-radius: 6px; + overflow: hidden; + font-size: 13px; +} + +.schedule-table th { + background: #2780E3; + color: white; + padding: 8px; + text-align: center; + font-weight: normal; +} + +.schedule-table td { + border: 1px solid #ddd; + padding: 6px; + vertical-align: top; +} + +.lesson-time { + font-weight: bold; + text-align: center; + white-space: nowrap; + background: #f5f5f5; +} + +.lesson-card { + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1px dashed #eee; +} + +.lesson-card:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.lesson-name { + font-weight: normal; + font-size: 12px; +} + +.lesson-type { + display: inline-block; + font-size: 10px; + color: #888; + margin-bottom: 2px; +} + +.lesson-room { + font-size: 10px; + color: #999; +} + +.lesson-teacher { + font-size: 10px; + color: #2780E3; + cursor: pointer; +} + +.lesson-teacher:hover { + text-decoration: underline; +} + +.no-lesson { + text-align: center; + color: #bbb; +} + +.empty-state { + text-align: center; + padding: 40px; + color: #999; +} + +@media (max-width: 768px) { + body { + padding: 10px; + } + + .form-row { + flex-direction: column; + } + + .form-group--button { + width: 100%; + } + + .btn { + width: 100%; + } + + .mode-switch { + flex-wrap: wrap; + } + + .week-nav { + flex-wrap: wrap; + } + + .schedule-table { + font-size: 11px; + } + + .schedule-table th, + .schedule-table td { + padding: 4px; + } +} + +@media (max-width: 480px) { + .header__title { + font-size: 18px; + } + + .header__subtitle { + font-size: 12px; + } +} + +@media print { + body { + background: white; + padding: 0; + } + + .mode-switch, + .search-panel, + .week-nav, + .btn, + .status { + display: none; + } + + .schedule-container { + margin: 0; + } +} \ No newline at end of file From 99827ed695fa3c9bdf211eb790e80f235c16a661 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Apr 2026 03:03:22 +0400 Subject: [PATCH 2/4] add gitignore --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..bc6eeb371 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +.venv/ +venv/ +env/ + +.vscode/ +.idea/ +.DS_Store + +*.log + +.env \ No newline at end of file From 38a9d67a8ce318d5e74759f2c1887cd53dacef35 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Apr 2026 03:13:22 +0400 Subject: [PATCH 3/4] fix gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bc6eeb371..bf221b00e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ __pycache__/ *.pyc -.venv/ +*.pyo + venv/ +.venv/ env/ .vscode/ From 20e57bf79075d2a7bc584cd296681bfec2fe4deb Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Apr 2026 03:15:19 +0400 Subject: [PATCH 4/4] delete pycache --- app/__pycache__/__init__.cpython-313.pyc | Bin 148 -> 0 bytes app/__pycache__/models.cpython-313.pyc | Bin 1829 -> 0 bytes app/__pycache__/parser.cpython-313.pyc | Bin 10831 -> 0 bytes app/__pycache__/ssau_client.cpython-313.pyc | Bin 8813 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/__pycache__/__init__.cpython-313.pyc delete mode 100644 app/__pycache__/models.cpython-313.pyc delete mode 100644 app/__pycache__/parser.cpython-313.pyc delete mode 100644 app/__pycache__/ssau_client.cpython-313.pyc diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 3a59645673f1a73d7d3cca5df60a30d734694bd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 148 zcmey&%ge<81d-=2WrFC(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl<>XRDad;?$zz zn1IC6oPhlFq{JAP)Z*-t{DPSB)TH9nWL=|}#DapD`1s7c%#!$cy@JYH95%W6DWy57 Xc15f}GeC9}gBTx~85tRin1L(+o|+?( diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc deleted file mode 100644 index 9c0dbfcc2f4ac697889935ac45c6b15b49f214fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1829 zcmaJ?J#X7a7(S9Gk)kA9j=!xqbyL`lh7@R%07cPuNtQZpVt@^@Fd!)TsxqNS^^R&P z6z$@n)l$eq0F7Py7y1*rIRXsgfT969d2^v5Yv1=s*^XnBfF8ad$NPAn_c=|fRhz)` z=gXJDHG`0U(3w7k+?f0X#tY&QN4rn9HLh*zT;Ddhv4JuDKHV_Yl~`sAz=vTix^vz|HY z4AnVK5tud`mMq`%`wno0l|Qb*8q$2u*t0#q4`uWdCM`rS5V15fqJ=6UkjEEr$BPI{ z2+Ig72&)LEd}LLtV+@O*9dsS7ccy>~i%kG1rz+yP@|~4xZn!mYkLYSP1AhEQsc=Dj zCVv22Rw^Zp;;FFA+@H#8>Q6;B^)snacj{+S1BvF}XHq-!GpU(w(aA(+xutxh5}9kc zwrjfPZ5^X|rmW;w;pKLl~O%L zUR4D^u9;iva`v5vFHdN;1k7`6$7R zSv(uRjQ1@}AmGtN2mrj#t&ZM1SRd1CX>DaRe^4FM^>fzZ`lxmAWK6H8^Yzi)gU`lv zBb{9y8G|3k^y)b)SKrJpKOEY16j*Ix_-J(NH)Hhifp=KjJffRf`{42q8YNkTdn<_A z>gLFL7Pl?SP`~WjW`Y+Xl6A4_Shf8(TUlD4X1CYp9RFK1_=(2=Cx)hJf0G-B^Zbc<*ZR>;psKmcISn3G>0co114*&oF diff --git a/app/__pycache__/parser.cpython-313.pyc b/app/__pycache__/parser.cpython-313.pyc deleted file mode 100644 index 2234f7c0d46af8a77368d104de68b64ceee4e577..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10831 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 diff --git a/app/__pycache__/ssau_client.cpython-313.pyc b/app/__pycache__/ssau_client.cpython-313.pyc deleted file mode 100644 index 352c9aa1b916a749620384944a9ef90b50da0799..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8813 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 Ke-q5*LH{q1#|NeW