Skip to content

Отчет по лабораторной работе №2

Выполнил: Шафиков Максим Азатович
Факультет: ПИН (ИКТ)
Группа: К3339
Преподаватель: Говоров Антон Игоревич


Тема и цель работы

Разработать веб‑сервис для бронирования отелей на базе FastAPI с использованием шаблонов Jinja2, СУБД PostgreSQL и ORM SQLAlchemy. Реализовать публичные страницы, личный кабинет, отзывы, административные разделы, пагинацию и основную бизнес‑логику бронирований.


Структура проекта

app/
  main.py                      # запуск приложения, middleware, обработчики ошибок
  pages/                       # пользовательские страницы и админ-панель
    router.py
  bookings/                    # маршруты бронирований
    router.py
  auth/                        # аутентификация и зависимости
    router.py
    deps.py
    utils.py
  crud/                        # слой работы с БД
    hotels.py
    bookings.py
    reviews.py
    users.py
  db/                          # модели и подключение к БД
    base.py
    models.py
    utils.py
  core/                        # конфигурация и безопасность
    config.py
    security.py
  templates/                   # Jinja2 шаблоны
    base.html, index.html, ...
  static/                      # статика и стили
    css/style.css
alembic.ini, app/alembic/      # миграции схемы БД
Dockerfile, docker-compose.yml # инфраструктура и запуск

Ключевые директории: - app/db — модели SQLAlchemy и базовые слои подключения. - app/crud — инкапсуляция запросов к БД (чтение/запись, пагинация, фильтрация). - app/pages и app/bookings — HTTP‑маршруты для страниц и операций бронирования. - app/auth — регистрация, логин, middleware‑аутентификация по cookie‑токену. - app/templates — шаблоны Jinja2 для UI.


Модели данных (основные)

# app/db/models.py (фрагменты)
class BookingStatus(enum.Enum):
    pending = "pending"
    confirmed = "confirmed"
    cancelled = "cancelled"
    checked_in = "checked_in"
    checked_out = "checked_out"

class Booking(Base):
    __tablename__ = "bookings"
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"))
    room_id = Column(Integer, ForeignKey("rooms.id"))
    check_in_date = Column(Date, nullable=False)
    check_out_date = Column(Date, nullable=False)
    status = Column(Enum(BookingStatus), default=BookingStatus.pending, nullable=False)

Модели User, Hotel, Room, Review описывают пользователей, отели, номера и отзывы соответственно. Связи настроены через relationship.


Основной запуск приложения и middleware

# app/main.py (фрагменты)
app = FastAPI(title="Hotel Booking Service")
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")

@app.on_event("startup")
async def startup_event():
    db: Session = next(get_db())
    create_initial_admin(db)
    db.close()

@app.middleware("http")
async def add_user_to_context(request: Request, call_next):
    user = await get_current_user_from_cookie(request, next(get_db()))
    request.state.user = user
    return await call_next(request)

При старте создается админ (если отсутствует). Middleware добавляет текущего пользователя в request.state.user для доступа в шаблонах.


Маршруты страниц и функционал

Главная страница /

Функции: - Отображение списка отелей с минимальной ценой номера и средним рейтингом. - Фильтры по цене, сортировка по рейтингу/цене. - Пагинация по 5 отелей с «диапазонной» навигацией.

# app/pages/router.py (фрагмент)
@router.get("/", response_class=HTMLResponse)
async def get_main_page(request: Request, db: Session = Depends(get_db),
                        page: int = 1, sort: str = Query("rating", enum=["rating","price_asc","price_desc"]),
                        min_price: str = Query(None), max_price: str = Query(None)):
    # безопасный парсинг фильтров цены
    min_price_float = float(min_price) if min_price and min_price.strip() else None
    max_price_float = float(max_price) if max_price and max_price.strip() else None

    hotels_with_stats, total_pages = hotel_repo.get_paginated(
        db=db, page=page, per_page=5, sort=sort,
        min_price=min_price_float, max_price=max_price_float,
    )
    return templates.TemplateResponse("index.html", {
        "request": request,
        "hotels": hotels_with_stats,
        "page": page,
        "total_pages": total_pages,
        "sort": sort,
        "min_price": min_price,
        "max_price": max_price,
    })

Пагинация отображает диапазоны элементов: «1‑5», «>> 6‑10» и т.д., при этом на первой/последней страницах лишние кнопки скрываются.

Страница отеля /hotel/{hotel_id}

Функции: - Детали отеля, список отзывов с пагинацией. - Для админа — список гостей за последний месяц (с пагинацией). - Форма добавления отзыва с валидацией дат: начало < конец, конец — в прошлом.

# app/pages/router.py (фрагмент)
@router.post("/hotel/{hotel_id}/review")
async def add_review(..., stay_start: date = Form(), stay_end: date = Form()):
    today = date.today()
    if stay_start >= stay_end:
        return RedirectResponse(url=f"/hotel/{hotel_id}?error=invalid_dates", status_code=302)
    if stay_end > today:
        return RedirectResponse(url=f"/hotel/{hotel_id}?error=future_stay", status_code=302)
    review_repo.create(...)
    return RedirectResponse(url=f"/hotel/{hotel_id}", status_code=302)

Личный кабинет /cabinet

Функции: - Список бронирований пользователя с пагинацией по 5. - Отмена будущих бронирований.

Админ‑панель /admin/bookings

Функции: - Список неподтвержденных бронирований с пагинацией по 5. - Подтверждение/отклонение заявок.


Бизнес‑логика в репозиториях

Репозиторий отелей: сортировка, фильтрация, пагинация

# app/crud/hotels.py (фрагмент)
q = (db.query(Hotel, func.min(Room.price).label("min_room_price"),
              func.avg(Review.rating.cast(Float)).label("avg_rating"))
       .outerjoin(Room, Room.hotel_id == Hotel.id)
       .outerjoin(Review, Review.hotel_id == Hotel.id)
       .group_by(Hotel.id))

if min_price is not None:
    q = q.having(func.min(Room.price) >= min_price)
if max_price is not None:
    q = q.having(func.min(Room.price) <= max_price)

if sort == "rating":
    q = q.order_by(func.coalesce(func.avg(Review.rating), 0).desc())
elif sort == "price_asc":
    q = q.order_by(func.coalesce(func.min(Room.price), 1e12).asc())
elif sort == "price_desc":
    q = q.order_by(func.coalesce(func.min(Room.price), 0).desc())

total = q.count()
items = q.offset((page - 1) * per_page).limit(per_page).all()

Репозиторий бронирований: выборки и админ‑операции

# app/crud/bookings.py (фрагмент)
def get_pending_paginated(self, db: Session, page: int, per_page: int):
    offset = (page - 1) * per_page
    query = db.query(models.Booking).filter(models.Booking.status == models.BookingStatus.pending)
    total = query.count()
    bookings = query.order_by(models.Booking.id).offset(offset).limit(per_page).all()
    return bookings, math.ceil(total / per_page)

Аутентификация

Реализована простая cookie‑аутентификация: 1. Пользователь логинится, сервер выдает JWT (подписанный SECRET_KEY) и кладет его в cookie access_token (HttpOnly). 2. Middleware декодирует токен, подставляет пользователя в request.state.user. 3. Декораторы‑зависимости защищают доступ к личному кабинету и админ‑панели.

# app/auth/deps.py (фрагмент)
async def get_current_user_from_cookie(request: Request, db: Session = Depends(get_db)):
    token = request.cookies.get("access_token")
    if not token:
        return None
    payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
    email = payload.get("sub")
    user = user_repo.get_by_email(db, email=email)
    return user

Пагинация (UI)

На страницах используется единый стиль «диапазонной» пагинации (по 5 элементов): - Левая кнопка скрыта на первой странице. - Правая скрыта на последней. - Текущий диапазон отображается как «i – i+4»; соседние диапазоны показываются в кнопках перехода.

Пример (фрагмент Jinja2):

{% set start_item = (page - 1) * 5 + 1 %}
{% set end_item = page * 5 %}
{% if page > 1 %}
  <a class="page-link" href="?page={{ page - 1 }}">{{ (page-2)*5+1 }} - {{ (page-1)*5 }} <<</a>
{% endif %}
<span class="page-link">{{ start_item }} - {{ end_item }}</span>
{% if page < total_pages %}
  <a class="page-link" href="?page={{ page + 1 }}">>> {{ page*5+1 }} - {{ (page+1)*5 }}</a>
{% endif %}

Валидация дат

  • При бронировании: дата заезда < даты выезда и заезд в будущем.
  • В отзывах: период проживания завершен (дата окончания в прошлом).
# app/bookings/router.py (фрагмент)
if check_in_date >= check_out_date:
    return RedirectResponse(url="/?error=invalid_dates", status_code=302)
if check_in_date < date.today():
    return RedirectResponse(url="/?error=past_checkin", status_code=302)

Сообщения об ошибках отображаются в шаблонах с помощью Bootstrap‑alert.


Сборка и запуск (Docker)

Сервис контейнеризирован. Запуск:

docker compose up --build -d

Приложение доступно по адресу http://localhost:8000 (или по адресу контейнера).


Вывод

В рамках ЛР №2 реализован веб‑сервис бронирования отелей: - Структура проекта разделена на слои (маршрутизация, репозитории, модели, шаблоны). - Реализованы фильтры, сортировка, пагинация с удобным UI. - Добавлены аутентификация, личный кабинет, отзывы, админ‑панель. Работа позволила повторить работу с Jinja2, FastAPI и Sqlalchemy