fix some stuff
This commit is contained in:
parent
1fa0ea2fd5
commit
ab642bc8b0
24 changed files with 846 additions and 15 deletions
BIN
!doc.tar.gz
Normal file
BIN
!doc.tar.gz
Normal file
Binary file not shown.
1
!doc/.~lock.fitnesscenter_report.md#
Normal file
1
!doc/.~lock.fitnesscenter_report.md#
Normal file
|
|
@ -0,0 +1 @@
|
|||
,q,think,23.03.2026 13:02,file:///home/q/.config/libreoffice/4;
|
||||
BIN
!doc/dashboard.png
Normal file
BIN
!doc/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
204
!doc/dashboard_auth.html
Normal file
204
!doc/dashboard_auth.html
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-bs-theme="dark">
|
||||
<head><base href="http://localhost:1337/">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Личный кабинет — FitnessCenter</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar navbar-expand-lg border-bottom bg-gradient position-sticky top-0 z-3 backdrop-blur">
|
||||
<div class="container-fluid px-4">
|
||||
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
|
||||
<img src="/static/img/logo.jpg" alt="logo" class="brand-logo">
|
||||
<span class="fw-bold">FitnessCenter</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Переключить меню">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<nav class="collapse navbar-collapse" id="mainNav">
|
||||
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-3">
|
||||
<li class="nav-item"><a class="nav-link" href="/">Главная</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#trainers">Тренеры</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#booking">Записаться</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#about">О клубе</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="/dashboard/">Кабинет</a></li>
|
||||
<li class="nav-item"><a class="btn btn-outline-light" href="/logout/">Выйти</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container py-4 py-lg-5">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card glass-card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="avatar bg-accent-subtle text-accent fw-bold me-3">Е</div>
|
||||
<div>
|
||||
<div class="fw-bold">Егор Тестов</div>
|
||||
<div class="text-secondary small">report@example.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">Активен до 15.04.2026</div>
|
||||
|
||||
<a class="btn btn-outline-light w-100" href="/logout/">Выйти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card glass-card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 fw-bold mb-3">Оформить абонемент</h2>
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="fw-bold">Старт 1 месяц</div>
|
||||
<div class="text-secondary small">30 дней</div>
|
||||
</div>
|
||||
<div class="fw-bold">2490,00 ₽</div>
|
||||
</div>
|
||||
<p class="text-secondary small mb-3">Базовый доступ ко всем зонам клуба.</p>
|
||||
<form method="post" action="/subscribe/1/">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
|
||||
<button class="btn btn-accent w-100" type="submit">Купить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="fw-bold">Сила 3 месяца</div>
|
||||
<div class="text-secondary small">90 дней</div>
|
||||
</div>
|
||||
<div class="fw-bold">6490,00 ₽</div>
|
||||
</div>
|
||||
<p class="text-secondary small mb-3">Три месяца + гостевой визит раз в месяц.</p>
|
||||
<form method="post" action="/subscribe/2/">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
|
||||
<button class="btn btn-accent w-100" type="submit">Купить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="fw-bold">Премиум 12 месяцев</div>
|
||||
<div class="text-secondary small">365 дней</div>
|
||||
</div>
|
||||
<div class="fw-bold">19900,00 ₽</div>
|
||||
</div>
|
||||
<p class="text-secondary small mb-3">Год без ограничений, приоритетная запись на тренировки.</p>
|
||||
<form method="post" action="/subscribe/3/">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
|
||||
<button class="btn btn-accent w-100" type="submit">Купить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card glass-card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 fw-bold mb-3">История</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>План</th>
|
||||
<th>Период</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>Старт 1 месяц</td>
|
||||
<td>16.03.2026 — 15.04.2026</td>
|
||||
<td>
|
||||
|
||||
<span class="badge text-bg-success">Активен</span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="footer py-5" id="booking">
|
||||
<div class="container">
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-md-4">
|
||||
<h4 class="fw-bold mb-3">Приобрести абонемент</h4>
|
||||
<p class="text-secondary">Оставьте контакты — менеджер подберёт план за пару минут.</p>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<form class="row g-3" aria-label="Заявка на абонемент">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="Ф.И.О" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="tel" class="form-control form-control-lg" placeholder="+7 (___) ___-__-__" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-select form-select-lg" required>
|
||||
<option value="" selected disabled>Выберите клуб</option>
|
||||
|
||||
<option>Центр на Большой Садовой — ул. Большая Садовая, 105</option>
|
||||
|
||||
<option>Западный Арена — пр-т Стачки, 190</option>
|
||||
|
||||
<option>Северный Loft — пр-т Космонавтов, 14/1</option>
|
||||
|
||||
<option>Левенцовка Energy — ул. Малиновского, 25Г</option>
|
||||
|
||||
<option>Береговой RiverFit — Береговая ул., 15к3</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 text-md-end">
|
||||
<button class="btn btn-accent btn-lg" type="button">Позвоните мне</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 small text-secondary">© 2026 FitnessCenter — здоровое тело как стиль жизни.</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
143
!doc/embed_docx_images.py
Normal file
143
!doc/embed_docx_images.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote, urlparse
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
PKG_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
||||
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
|
||||
|
||||
ET.register_namespace("", PKG_NS)
|
||||
ET.register_namespace("a", A_NS)
|
||||
ET.register_namespace("r", R_NS)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Embed externally linked local images into a DOCX file."
|
||||
)
|
||||
parser.add_argument("input_docx", type=Path)
|
||||
parser.add_argument("output_docx", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def local_path_from_target(target: str) -> Path | None:
|
||||
parsed = urlparse(target)
|
||||
if parsed.scheme != "file":
|
||||
return None
|
||||
return Path(unquote(parsed.path))
|
||||
|
||||
|
||||
def ensure_content_type(defaults_root: ET.Element, extension: str) -> None:
|
||||
ext = extension.lower().lstrip(".")
|
||||
existing = defaults_root.find(f"{{{CT_NS}}}Default[@Extension='{ext}']")
|
||||
if existing is not None:
|
||||
return
|
||||
|
||||
content_types = {
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"gif": "image/gif",
|
||||
"bmp": "image/bmp",
|
||||
"svg": "image/svg+xml",
|
||||
}
|
||||
content_type = content_types.get(ext)
|
||||
if not content_type:
|
||||
return
|
||||
|
||||
ET.SubElement(
|
||||
defaults_root,
|
||||
f"{{{CT_NS}}}Default",
|
||||
{"Extension": ext, "ContentType": content_type},
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_docx = args.input_docx.resolve()
|
||||
output_docx = args.output_docx.resolve()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workdir = Path(tmpdir)
|
||||
with zipfile.ZipFile(input_docx) as zin:
|
||||
zin.extractall(workdir)
|
||||
|
||||
rels_path = workdir / "word" / "_rels" / "document.xml.rels"
|
||||
doc_path = workdir / "word" / "document.xml"
|
||||
types_path = workdir / "[Content_Types].xml"
|
||||
media_dir = workdir / "word" / "media"
|
||||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rels_tree = ET.parse(rels_path)
|
||||
rels_root = rels_tree.getroot()
|
||||
doc_tree = ET.parse(doc_path)
|
||||
doc_root = doc_tree.getroot()
|
||||
types_tree = ET.parse(types_path)
|
||||
types_root = types_tree.getroot()
|
||||
|
||||
rel_to_target: dict[str, str] = {}
|
||||
next_image_num = 1
|
||||
|
||||
existing_media = sorted(media_dir.glob("image*.*"))
|
||||
if existing_media:
|
||||
next_image_num = len(existing_media) + 1
|
||||
|
||||
for rel in rels_root.findall(f"{{{PKG_NS}}}Relationship"):
|
||||
rel_type = rel.attrib.get("Type", "")
|
||||
target_mode = rel.attrib.get("TargetMode")
|
||||
if not rel_type.endswith("/image") or target_mode != "External":
|
||||
continue
|
||||
|
||||
target = rel.attrib.get("Target", "")
|
||||
local_path = local_path_from_target(target)
|
||||
if not local_path or not local_path.exists():
|
||||
continue
|
||||
|
||||
image_name = f"image{next_image_num}{local_path.suffix.lower()}"
|
||||
next_image_num += 1
|
||||
embedded_target = f"media/{image_name}"
|
||||
shutil.copy2(local_path, media_dir / image_name)
|
||||
|
||||
rel.attrib["Target"] = embedded_target
|
||||
rel.attrib.pop("TargetMode", None)
|
||||
rel_to_target[rel.attrib["Id"]] = embedded_target
|
||||
ensure_content_type(types_root, local_path.suffix)
|
||||
|
||||
if not rel_to_target:
|
||||
raise SystemExit("No external local images found in DOCX relationships.")
|
||||
|
||||
for blip in doc_root.findall(f".//{{{A_NS}}}blip"):
|
||||
link_key = f"{{{R_NS}}}link"
|
||||
embed_key = f"{{{R_NS}}}embed"
|
||||
rel_id = blip.attrib.get(link_key)
|
||||
if rel_id in rel_to_target:
|
||||
blip.attrib[embed_key] = rel_id
|
||||
blip.attrib.pop(link_key, None)
|
||||
|
||||
rels_tree.write(rels_path, encoding="UTF-8", xml_declaration=True)
|
||||
doc_tree.write(doc_path, encoding="UTF-8", xml_declaration=True)
|
||||
types_tree.write(types_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
if output_docx.exists():
|
||||
output_docx.unlink()
|
||||
|
||||
with zipfile.ZipFile(output_docx, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||
for path in sorted(workdir.rglob("*")):
|
||||
if path.is_dir():
|
||||
continue
|
||||
zout.write(path, path.relative_to(workdir).as_posix())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
!doc/example.docx
Normal file
BIN
!doc/example.docx
Normal file
Binary file not shown.
BIN
!doc/fitnesscenter_report.docx
Normal file
BIN
!doc/fitnesscenter_report.docx
Normal file
Binary file not shown.
143
!doc/fitnesscenter_report.md
Normal file
143
!doc/fitnesscenter_report.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Отчёт по проекту FitnessCenter
|
||||
|
||||
Подготовлено на основе технической документации проекта, работающего локального экземпляра сайта и шаблона оформления `example.docx`.
|
||||
|
||||
Город: Ростов-на-Дону
|
||||
Дата подготовки отчёта: 16.03.2026
|
||||
|
||||
## Введение
|
||||
|
||||
Проект **FitnessCenter** представляет собой веб-приложение фитнес-клуба с лендингом, регистрацией пользователей, авторизацией, личным кабинетом и оформлением абонементов. Система ориентирована на быстрый запуск в учебной или демонстрационной среде и построена по модели серверного рендеринга HTML без отдельного JSON API.
|
||||
|
||||
Целью работы команды было создание понятного и визуально цельного сайта, который одновременно решает маркетинговую задачу лендинга и прикладную задачу оформления абонементов через личный кабинет.
|
||||
|
||||
## Краткое описание системы
|
||||
|
||||
В ходе анализа технической документации и интерфейса были выделены следующие ключевые возможности проекта:
|
||||
|
||||
- главная страница с презентацией клуба, тарифов, тренеров и филиалов;
|
||||
- регистрация нового пользователя с проверкой телефона в формате `+7XXXXXXXXXX`;
|
||||
- вход в систему через форму авторизации;
|
||||
- личный кабинет с отображением активного абонемента и истории покупок;
|
||||
- оформление нового абонемента из личного кабинета;
|
||||
- административное управление данными через стандартную Django-админку;
|
||||
- запуск в Docker-контуре с публикацией сервиса на порту `1337`.
|
||||
|
||||
## Технологический стек и архитектура
|
||||
|
||||
По технической документации проект реализован на следующем стеке:
|
||||
|
||||
- backend: `Python 3.13`, `Django 5.1.4`;
|
||||
- frontend: `Bootstrap 5.3`, кастомные стили в `static/css/main.css`, шрифт `Manrope`;
|
||||
- база данных по умолчанию: `SQLite`, с возможностью переноса на `PostgreSQL`;
|
||||
- инфраструктура: `Dockerfile`, `docker-compose.yml`, `entrypoint.sh`;
|
||||
- архитектурный подход: серверный рендеринг HTML-страниц без выделенного REST API.
|
||||
|
||||
Ключевые структурные элементы проекта:
|
||||
|
||||
- `gymclub/` содержит настройки Django и верхнеуровневую маршрутизацию;
|
||||
- `club/` реализует модели, формы, views и бизнес-логику абонементов;
|
||||
- `templates/` содержит шаблоны страниц `home`, `register`, `login`, `dashboard`;
|
||||
- `static/` и `staticfiles/` отвечают за визуальную часть проекта;
|
||||
- маршруты регистрации, входа, выхода и покупки абонементов сосредоточены в `club/urls.py`.
|
||||
|
||||
## Реализованный функционал
|
||||
|
||||
В итоговой версии системы реализованы следующие пользовательские сценарии:
|
||||
|
||||
### 1. Лендинг фитнес-клуба
|
||||
|
||||
Главная страница знакомит пользователя с клубом, доступными тарифами, тренерами и адресами филиалов. Интерфейс выполнен в тёмной цветовой схеме с акцентным зелёным цветом, что делает сайт визуально единым и современным.
|
||||
|
||||
### 2. Регистрация и авторизация
|
||||
|
||||
Пользователь может создать аккаунт, указав логин, имя, фамилию, телефон, email и пароль. После регистрации доступны вход в систему и переход в личный кабинет. Для телефона реализована нормализация и валидация, что снижает количество некорректных записей.
|
||||
|
||||
### 3. Личный кабинет и покупка абонементов
|
||||
|
||||
После авторизации пользователь видит активный абонемент, историю покупок и перечень доступных тарифов. Покупка выполняется через серверный сценарий, который рассчитывает дату окончания абонемента автоматически.
|
||||
|
||||
### 4. Эксплуатационная готовность
|
||||
|
||||
Проект запускается в контейнере Docker, а настройки окружения вынесены в переменные. Это упрощает перенос между стендами и делает демонстрацию предсказуемой.
|
||||
|
||||
## Команда проекта и распределение работ
|
||||
|
||||
Оценка трудозатрат ниже является проектной и отражает предполагаемый вклад участников на протяжении основного этапа разработки с **10.02.2026 по 14.03.2026**.
|
||||
|
||||
- **Комин Архип**
|
||||
Роль: тимлид, инфраструктура, DevOps и backend.
|
||||
Оценка занятости: **64 часа**.
|
||||
Основные задачи: постановка архитектуры проекта, подготовка Docker-контура, настройка переменных окружения, организация структуры Django-приложения, реализация моделей `MembershipPlan`, `Profile`, `Subscription`, настройка маршрутов и контроль интеграции backend-части с шаблонами.
|
||||
|
||||
- **Токмачев Егор**
|
||||
Роль: full-stack разработчик.
|
||||
Оценка занятости: **58 часов**.
|
||||
Основные задачи: реализация форм регистрации и входа, связка пользовательских сценариев с бизнес-логикой, доработка личного кабинета, интеграция покупки абонементов, отладка переходов между страницами и проверка корректности пользовательских сообщений.
|
||||
|
||||
- **Стрельников Вадим**
|
||||
Роль: frontend разработчик, дизайнер.
|
||||
Оценка занятости: **49 часов**.
|
||||
Основные задачи: разработка визуальной концепции сайта, вёрстка главной страницы и экранов авторизации, настройка тёмной темы, адаптация карточек тарифов и тренеров, подбор визуальных акцентов, доработка читаемости и согласованности интерфейса на разных экранах.
|
||||
|
||||
- **Акуев Магомед**
|
||||
Роль: тестировщик, дизайнер.
|
||||
Оценка занятости: **37 часов**.
|
||||
Основные задачи: ручное тестирование регистрации, входа и кабинета, проверка сценариев покупки абонемента, поиск визуальных и функциональных дефектов, проверка текстов форм и состояний ошибок, участие в финальной полировке элементов интерфейса.
|
||||
|
||||
**Суммарная оценка трудозатрат команды: 208 часов.**
|
||||
|
||||
## Итоговые экраны системы
|
||||
|
||||
Ниже приведены скриншоты реально работающего локального экземпляра проекта.
|
||||
|
||||
### Главная страница
|
||||
|
||||

|
||||
|
||||
На главной странице представлены ключевое позиционирование клуба, тарифы, карточки тренеров, адреса филиалов и информационные блоки.
|
||||
|
||||
### Страница входа
|
||||
|
||||

|
||||
|
||||
Экран входа минималистичен и сфокусирован на быстрой авторизации пользователя.
|
||||
|
||||
### Страница регистрации
|
||||
|
||||

|
||||
|
||||
Форма регистрации включает обязательные пользовательские поля и согласие на обработку персональных данных.
|
||||
|
||||
### Личный кабинет пользователя
|
||||
|
||||

|
||||
|
||||
В кабинете отображаются профиль пользователя, срок действия активного абонемента, доступные тарифы и история покупок.
|
||||
|
||||
## Вывод
|
||||
|
||||
По результатам анализа документации и проверки интерфейса можно сделать вывод, что проект **FitnessCenter** доведён до работоспособного состояния и решает основные задачи учебного веб-продукта: демонстрацию услуг фитнес-клуба, регистрацию пользователей, авторизацию и оформление абонементов. Архитектура проекта остаётся достаточно простой для сопровождения, а Docker-окружение делает запуск и демонстрацию удобными.
|
||||
|
||||
Отдельным преимуществом проекта является согласованная работа команды: backend, frontend, дизайн и тестирование разделены по ролям, но при этом объединены в единый пользовательский сценарий. Это делает результат целостным как с технической, так и с визуальной точки зрения.
|
||||
|
||||
## Использованные материалы и литература
|
||||
|
||||
### Локальные материалы проекта
|
||||
|
||||
- техническая документация проекта: `../DOCS_RU.md`;
|
||||
- локально запущенный сайт: `http://localhost:1337`;
|
||||
- шаблон оформления и структуры отчёта: `example.docx`.
|
||||
|
||||
### Подобранная литература
|
||||
|
||||
1. Django Software Foundation. **Django documentation** [Электронный ресурс]. URL: `https://docs.djangoproject.com/en/5.1/` (дата обращения: 16.03.2026).
|
||||
2. Bootstrap Team. **Bootstrap v5.3 Documentation** [Электронный ресурс]. URL: `https://getbootstrap.com/docs/5.3/` (дата обращения: 16.03.2026).
|
||||
3. Fowler M. **Patterns of Enterprise Application Architecture**. Boston: Addison-Wesley, 2002.
|
||||
4. Krug S. **Don't Make Me Think, Revisited: A Common Sense Approach to Web Usability**. 3rd ed. Berkeley: New Riders, 2014.
|
||||
5. Pressman R. S., Maxim B. R. **Software Engineering: A Practitioner's Approach**. 9th ed. New York: McGraw-Hill, 2019.
|
||||
6. Gamma E., Helm R., Johnson R., Vlissides J. **Design Patterns: Elements of Reusable Object-Oriented Software**. Boston: Addison-Wesley, 1994.
|
||||
7. Sommerville I. **Software Engineering**. 10th ed. Boston: Pearson, 2015.
|
||||
8. Нильсен Я. **Веб-дизайн: удобство использования веб-сайтов**. М.: Вильямс, 2010.
|
||||
9. Купер А., Рейман Р., Кронин Д., Носсел К. **Интерфейс. Основы проектирования взаимодействия**. 4-е изд. СПб.: Питер, 2017.
|
||||
10. Макконнелл С. **Совершенный код**. 2-е изд. СПб.: Питер, 2019.
|
||||
94
!doc/fitnesscenter_reporte.docx.xml
Normal file
94
!doc/fitnesscenter_reporte.docx.xml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<indexing>
|
||||
<object alt="" name="Image1" object_type="graphic"/>
|
||||
<object alt="" name="Image2" object_type="graphic"/>
|
||||
<object alt="" name="Image3" object_type="graphic"/>
|
||||
<object alt="" name="Image4" object_type="graphic"/>
|
||||
<paragraph index="21" node_type="writer">Отчёт по проекту FitnessCenter</paragraph>
|
||||
<paragraph index="22" node_type="writer">Подготовлено на основе технической документации проекта, работающего локального экземпляра сайта и шаблона оформления example.docx.</paragraph>
|
||||
<paragraph index="23" node_type="writer">Город: Ростов-на-Дону
|
||||
Дата подготовки отчёта: 16.03.2026</paragraph>
|
||||
<paragraph index="24" node_type="writer">Введение</paragraph>
|
||||
<paragraph index="25" node_type="writer">Проект FitnessCenter представляет собой веб-приложение фитнес-клуба с лендингом, регистрацией пользователей, авторизацией, личным кабинетом и оформлением абонементов. Система ориентирована на быстрый запуск в учебной или демонстрационной среде и построена по модели серверного рендеринга HTML без отдельного JSON API.</paragraph>
|
||||
<paragraph index="26" node_type="writer">Целью работы команды было создание понятного и визуально цельного сайта, который одновременно решает маркетинговую задачу лендинга и прикладную задачу оформления абонементов через личный кабинет.</paragraph>
|
||||
<paragraph index="27" node_type="writer">Краткое описание системы</paragraph>
|
||||
<paragraph index="28" node_type="writer">В ходе анализа технической документации и интерфейса были выделены следующие ключевые возможности проекта:</paragraph>
|
||||
<paragraph index="29" node_type="writer">главная страница с презентацией клуба, тарифов, тренеров и филиалов;</paragraph>
|
||||
<paragraph index="30" node_type="writer">регистрация нового пользователя с проверкой телефона в формате +7XXXXXXXXXX;</paragraph>
|
||||
<paragraph index="31" node_type="writer">вход в систему через форму авторизации;</paragraph>
|
||||
<paragraph index="32" node_type="writer">личный кабинет с отображением активного абонемента и истории покупок;</paragraph>
|
||||
<paragraph index="33" node_type="writer">оформление нового абонемента из личного кабинета;</paragraph>
|
||||
<paragraph index="34" node_type="writer">административное управление данными через стандартную Django-админку;</paragraph>
|
||||
<paragraph index="35" node_type="writer">запуск в Docker-контуре с публикацией сервиса на порту 1337.</paragraph>
|
||||
<paragraph index="36" node_type="writer">Технологический стек и архитектура</paragraph>
|
||||
<paragraph index="37" node_type="writer">По технической документации проект реализован на следующем стеке:</paragraph>
|
||||
<paragraph index="38" node_type="writer">backend: Python 3.13, Django 5.1.4;</paragraph>
|
||||
<paragraph index="39" node_type="writer">frontend: Bootstrap 5.3, кастомные стили в static/css/main.css, шрифт Manrope;</paragraph>
|
||||
<paragraph index="40" node_type="writer">база данных по умолчанию: SQLite, с возможностью переноса на PostgreSQL;</paragraph>
|
||||
<paragraph index="41" node_type="writer">инфраструктура: Dockerfile, docker-compose.yml, entrypoint.sh;</paragraph>
|
||||
<paragraph index="42" node_type="writer">архитектурный подход: серверный рендеринг HTML-страниц без выделенного REST API.</paragraph>
|
||||
<paragraph index="43" node_type="writer">Ключевые структурные элементы проекта:</paragraph>
|
||||
<paragraph index="44" node_type="writer">gymclub/ содержит настройки Django и верхнеуровневую маршрутизацию;</paragraph>
|
||||
<paragraph index="45" node_type="writer">club/ реализует модели, формы, views и бизнес-логику абонементов;</paragraph>
|
||||
<paragraph index="46" node_type="writer">templates/ содержит шаблоны страниц home, register, login, dashboard;</paragraph>
|
||||
<paragraph index="47" node_type="writer">static/ и staticfiles/ отвечают за визуальную часть проекта;</paragraph>
|
||||
<paragraph index="48" node_type="writer">маршруты регистрации, входа, выхода и покупки абонементов сосредоточены в club/urls.py.</paragraph>
|
||||
<paragraph index="49" node_type="writer">Реализованный функционал</paragraph>
|
||||
<paragraph index="50" node_type="writer">В итоговой версии системы реализованы следующие пользовательские сценарии:</paragraph>
|
||||
<paragraph index="51" node_type="writer">1. Лендинг фитнес-клуба</paragraph>
|
||||
<paragraph index="52" node_type="writer">Главная страница знакомит пользователя с клубом, доступными тарифами, тренерами и адресами филиалов. Интерфейс выполнен в тёмной цветовой схеме с акцентным зелёным цветом, что делает сайт визуально единым и современным.</paragraph>
|
||||
<paragraph index="53" node_type="writer">2. Регистрация и авторизация</paragraph>
|
||||
<paragraph index="54" node_type="writer">Пользователь может создать аккаунт, указав логин, имя, фамилию, телефон, email и пароль. После регистрации доступны вход в систему и переход в личный кабинет. Для телефона реализована нормализация и валидация, что снижает количество некорректных записей.</paragraph>
|
||||
<paragraph index="55" node_type="writer">3. Личный кабинет и покупка абонементов</paragraph>
|
||||
<paragraph index="56" node_type="writer">После авторизации пользователь видит активный абонемент, историю покупок и перечень доступных тарифов. Покупка выполняется через серверный сценарий, который рассчитывает дату окончания абонемента автоматически.</paragraph>
|
||||
<paragraph index="57" node_type="writer">4. Эксплуатационная готовность</paragraph>
|
||||
<paragraph index="58" node_type="writer">Проект запускается в контейнере Docker, а настройки окружения вынесены в переменные. Это упрощает перенос между стендами и делает демонстрацию предсказуемой.</paragraph>
|
||||
<paragraph index="59" node_type="writer">Команда проекта и распределение работ</paragraph>
|
||||
<paragraph index="60" node_type="writer">Оценка трудозатрат ниже является проектной и отражает предполагаемый вклад участников на протяжении основного этапа разработки с 10.02.2026 по 14.03.2026.</paragraph>
|
||||
<paragraph index="61" node_type="writer">Комин Архип
|
||||
Роль: тимлид, инфраструктура, DevOps и backend.
|
||||
Оценка занятости: 64 часа.
|
||||
Основные задачи: постановка архитектуры проекта, подготовка Docker-контура, настройка переменных окружения, организация структуры Django-приложения, реализация моделей MembershipPlan, Profile, Subscription, настройка маршрутов и контроль интеграции backend-части с шаблонами.</paragraph>
|
||||
<paragraph index="62" node_type="writer">Токмачев Егор
|
||||
Роль: full-stack разработчик.
|
||||
Оценка занятости: 58 часов.
|
||||
Основные задачи: реализация форм регистрации и входа, связка пользовательских сценариев с бизнес-логикой, доработка личного кабинета, интеграция покупки абонементов, отладка переходов между страницами и проверка корректности пользовательских сообщений.</paragraph>
|
||||
<paragraph index="63" node_type="writer">Стрельников Вадим
|
||||
Роль: frontend разработчик, дизайнер.
|
||||
Оценка занятости: 49 часов.
|
||||
Основные задачи: разработка визуальной концепции сайта, вёрстка главной страницы и экранов авторизации, настройка тёмной темы, адаптация карточек тарифов и тренеров, подбор визуальных акцентов, доработка читаемости и согласованности интерфейса на разных экранах.</paragraph>
|
||||
<paragraph index="64" node_type="writer">Акуев Магомед
|
||||
Роль: тестировщик, дизайнер.
|
||||
Оценка занятости: 37 часов.
|
||||
Основные задачи: ручное тестирование регистрации, входа и кабинета, проверка сценариев покупки абонемента, поиск визуальных и функциональных дефектов, проверка текстов форм и состояний ошибок, участие в финальной полировке элементов интерфейса.</paragraph>
|
||||
<paragraph index="65" node_type="writer">Суммарная оценка трудозатрат команды: 208 часов.</paragraph>
|
||||
<paragraph index="66" node_type="writer">Итоговые экраны системы</paragraph>
|
||||
<paragraph index="67" node_type="writer">Ниже приведены скриншоты реально работающего локального экземпляра проекта.</paragraph>
|
||||
<paragraph index="68" node_type="writer">Главная страница</paragraph>
|
||||
<paragraph index="70" node_type="writer">На главной странице представлены ключевое позиционирование клуба, тарифы, карточки тренеров, адреса филиалов и информационные блоки.</paragraph>
|
||||
<paragraph index="71" node_type="writer">Страница входа</paragraph>
|
||||
<paragraph index="73" node_type="writer">Экран входа минималистичен и сфокусирован на быстрой авторизации пользователя.</paragraph>
|
||||
<paragraph index="74" node_type="writer">Страница регистрации</paragraph>
|
||||
<paragraph index="76" node_type="writer">Форма регистрации включает обязательные пользовательские поля и согласие на обработку персональных данных.</paragraph>
|
||||
<paragraph index="77" node_type="writer">Личный кабинет пользователя</paragraph>
|
||||
<paragraph index="79" node_type="writer">В кабинете отображаются профиль пользователя, срок действия активного абонемента, доступные тарифы и история покупок.</paragraph>
|
||||
<paragraph index="80" node_type="writer">Вывод</paragraph>
|
||||
<paragraph index="81" node_type="writer">По результатам анализа документации и проверки интерфейса можно сделать вывод, что проект FitnessCenter доведён до работоспособного состояния и решает основные задачи учебного веб-продукта: демонстрацию услуг фитнес-клуба, регистрацию пользователей, авторизацию и оформление абонементов. Архитектура проекта остаётся достаточно простой для сопровождения, а Docker-окружение делает запуск и демонстрацию удобными.</paragraph>
|
||||
<paragraph index="82" node_type="writer">Отдельным преимуществом проекта является согласованная работа команды: backend, frontend, дизайн и тестирование разделены по ролям, но при этом объединены в единый пользовательский сценарий. Это делает результат целостным как с технической, так и с визуальной точки зрения.</paragraph>
|
||||
<paragraph index="83" node_type="writer">Использованные материалы и литература</paragraph>
|
||||
<paragraph index="84" node_type="writer">Локальные материалы проекта</paragraph>
|
||||
<paragraph index="85" node_type="writer">техническая документация проекта: ../DOCS_RU.md;</paragraph>
|
||||
<paragraph index="86" node_type="writer">локально запущенный сайт: http://localhost:1337;</paragraph>
|
||||
<paragraph index="87" node_type="writer">шаблон оформления и структуры отчёта: example.docx.</paragraph>
|
||||
<paragraph index="88" node_type="writer">Подобранная литература</paragraph>
|
||||
<paragraph index="89" node_type="writer">Django Software Foundation. Django documentation [Электронный ресурс]. URL: https://docs.djangoproject.com/en/5.1/ (дата обращения: 16.03.2026).</paragraph>
|
||||
<paragraph index="90" node_type="writer">Bootstrap Team. Bootstrap v5.3 Documentation [Электронный ресурс]. URL: https://getbootstrap.com/docs/5.3/ (дата обращения: 16.03.2026).</paragraph>
|
||||
<paragraph index="91" node_type="writer">Fowler M. Patterns of Enterprise Application Architecture. Boston: Addison-Wesley, 2002.</paragraph>
|
||||
<paragraph index="92" node_type="writer">Krug S. Don't Make Me Think, Revisited: A Common Sense Approach to Web Usability. 3rd ed. Berkeley: New Riders, 2014.</paragraph>
|
||||
<paragraph index="93" node_type="writer">Pressman R. S., Maxim B. R. Software Engineering: A Practitioner's Approach. 9th ed. New York: McGraw-Hill, 2019.</paragraph>
|
||||
<paragraph index="94" node_type="writer">Gamma E., Helm R., Johnson R., Vlissides J. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley, 1994.</paragraph>
|
||||
<paragraph index="95" node_type="writer">Sommerville I. Software Engineering. 10th ed. Boston: Pearson, 2015.</paragraph>
|
||||
<paragraph index="96" node_type="writer">Нильсен Я. Веб-дизайн: удобство использования веб-сайтов. М.: Вильямс, 2010.</paragraph>
|
||||
<paragraph index="97" node_type="writer">Купер А., Рейман Р., Кронин Д., Носсел К. Интерфейс. Основы проектирования взаимодействия. 4-е изд. СПб.: Питер, 2017.</paragraph>
|
||||
<paragraph index="98" node_type="writer">Макконнелл С. Совершенный код. 2-е изд. СПб.: Питер, 2019.</paragraph>
|
||||
</indexing>
|
||||
BIN
!doc/home.png
Normal file
BIN
!doc/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 584 KiB |
BIN
!doc/login.png
Normal file
BIN
!doc/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
!doc/register.png
Normal file
BIN
!doc/register.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
91
DOCS_RU.md
Normal file
91
DOCS_RU.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# FitnessCenter — эксплуатационная документация
|
||||
|
||||
## Назначение
|
||||
Одностраничный лендинг и личный кабинет фитнес‑клуба с регистрацией, входом и покупкой абонементов. Рендеринг на сервере (Django), без JSON API.
|
||||
|
||||
## Стек
|
||||
- Python 3.13, Django 5.1.4
|
||||
- Bootstrap 5.3 (CDN), шрифт Manrope, кастомный тёмный UI `static/css/main.css`
|
||||
- БД по умолчанию: SQLite (`db.sqlite3`); совместимо с Postgres
|
||||
- Контейнеризация: Dockerfile (python:3.13-slim) + `docker-compose.yml` (порт 1337→8000)
|
||||
|
||||
## Архитектура и файлы
|
||||
- `gymclub/` — настройки Django, URL‑маршрутизация верхнего уровня.
|
||||
- `club/` — доменная логика: модели, формы, views, urls, context processor с адресами клубов.
|
||||
- `templates/` — серверные шаблоны: `base.html`, `home.html`, `register.html`, `login.html`, `dashboard.html`.
|
||||
- `static/` — стили и логотип, подключаются через `{% static %}`; собранные файлы кладутся в `staticfiles/`.
|
||||
- `entrypoint.sh` — миграции + `collectstatic` (ошибки игнорируются) и запуск `runserver` на 0.0.0.0:8000.
|
||||
- `www/` — архивная статическая версия лендинга (не используется приложением).
|
||||
|
||||
## Данные и бизнес‑логика
|
||||
- `MembershipPlan`: имя, описание, длительность в днях, цена. Начальные тарифы создаются миграцией `0002_default_plans`.
|
||||
- `Profile`: OneToOne к пользователю, хранит телефон.
|
||||
- `Subscription`: ссылка на пользователя и план, даты начала/окончания; `is_active` проверяет `end_date` ≥ сегодня.
|
||||
- Хелпер `Subscription.create_for_plan(user, plan)` рассчитывает дату окончания от текущей даты и создаёт запись — используйте его вместо ручного сохранения.
|
||||
- Проверка телефона (RegistrationForm): нормализует «8» в «+7», требует 11 цифр формата +7XXXXXXXXXX.
|
||||
- Парольные валидаторы стандартные, но проверка «простых паролей» отключена (CommonPasswordValidator не подключён).
|
||||
|
||||
## Маршруты (HTML API)
|
||||
Все пути объявлены в `club/urls.py`, подключены в `gymclub/urls.py`.
|
||||
- `GET /` — витрина с планами, адресами клубов.
|
||||
- `GET /register/`, `POST /register/` — регистрация пользователя, автологин, создание Profile.
|
||||
- `GET /login/`, `POST /login/` — аутентификация через Django AuthenticationForm.
|
||||
- `GET /logout/` — logout + redirect домой (view использует `logout`, форма не нужна).
|
||||
- `GET /dashboard/` (требует авторизации) — активный абонемент, история, покупка новых.
|
||||
- `POST /subscribe/<int:plan_id>/` — покупка плана через `Subscription.create_for_plan`.
|
||||
- `admin/` — стандартная админка для планов, подписок и профилей.
|
||||
Все формы защищены CSRF; сообщения пользователю выводятся через Django messages.
|
||||
|
||||
## UI и контент
|
||||
- Тёмная тема с акцентным цветом `--accent` в `static/css/main.css`; поменяйте переменные в корне файла для ребрендинга.
|
||||
- Список клубов задаётся константой `CLUBS` в `club/context_processors.py` и доступен в шаблонах как `clubs`.
|
||||
- Главная страница содержит секции «Абонементы», «Тренеры», «Клубы», «О нас» и форму заявки в футере (пока без обработки).
|
||||
|
||||
## Переменные окружения
|
||||
Файл примера `.env.example`:
|
||||
- `DJANGO_DEBUG` (True/False)
|
||||
- `DJANGO_ALLOWED_HOSTS` (список через запятую, `*` по умолчанию)
|
||||
- `DJANGO_SECRET_KEY` (смените в проде)
|
||||
Дополнительно в `settings.py`: язык `ru-ru`, часовой пояс `Europe/Moscow`.
|
||||
|
||||
## Запуск
|
||||
### В Docker
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose build
|
||||
docker compose up
|
||||
# Откройте http://localhost:1337
|
||||
```
|
||||
Контейнер монтирует текущую директорию, так что правки видны без пересборки.
|
||||
|
||||
### Локально (без Docker)
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env # при необходимости отредактируйте
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### Админка и данные
|
||||
- Создайте суперпользователя: `python manage.py createsuperuser`.
|
||||
- Начальные планы создаются миграцией; новые можно добавить через /admin/ или фикстуры.
|
||||
|
||||
### Переключение на Postgres (кратко)
|
||||
1. Добавьте сервис в `docker-compose.yml` или поднимите внешнюю БД.
|
||||
2. В `gymclub/settings.py` задайте `ENGINE = 'django.db.backends.postgresql'` и параметры `NAME/USER/PASSWORD/HOST/PORT` (лучше через переменные окружения).
|
||||
3. Выполните миграции заново (`python manage.py migrate`).
|
||||
|
||||
## Хитрости и рекомендации
|
||||
- Используйте `Subscription.create_for_plan` — он правильно считает `end_date` от текущего дня.
|
||||
- Телефон хранится в нормализованном формате `+7XXXXXXXXXX`; это можно использовать для SMS‑интеграций.
|
||||
- Планы отсортированы по цене (`ordering = ['price']`), поэтому вывод на главной будет от дешёвого к дорогому.
|
||||
- `entrypoint.sh` не падает, если `collectstatic` без настроек S3; удобнее для локалки, но в проде задайте `STATIC_ROOT` и уберите `|| true` при необходимости.
|
||||
- Блок футера «Позвоните мне» пока статический — если нужна отправка заявки, повесьте action на форму и добавьте view.
|
||||
|
||||
## Полезные пути
|
||||
- Основной код: `club/views.py`, `club/forms.py`, `club/models.py`
|
||||
- Стили: `static/css/main.css`
|
||||
- Шаблоны: `templates/`
|
||||
- Контейнеризация: `Dockerfile`, `docker-compose.yml`, `entrypoint.sh`
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
- Frontend: Bootstrap 5 (CDN), шрифт Manrope (Google Fonts), кастомный CSS `static/css/main.css`.
|
||||
- БД по умолчанию: SQLite (`db.sqlite3`). Настроена смена на Postgres при необходимости.
|
||||
- Docker: `python:3.13-slim`, entrypoint выполняет миграции и стартует `runserver`.
|
||||
- Встроенный список клубов (Ростов-на-Дону) доступен во всех шаблонах через context processor.
|
||||
|
||||
## Маршруты и HTTP‑запросы
|
||||
Все маршруты определены в `club/urls.py` и подключены через `gymclub/urls.py`.
|
||||
|
|
@ -24,12 +25,15 @@
|
|||
- Все формы используют CSRF‑токен.
|
||||
- Сообщения пользователю выводятся через Django messages (успех/ошибка).
|
||||
- API JSON не реализован; взаимодействие сервер‑рендерингом HTML.
|
||||
- Проверка паролей упрощена: отключена `CommonPasswordValidator` (часто используемые пароли не блокируются).
|
||||
- Телефон проверяется на реальный российский номер: формат +7XXXXXXXXXX, сохраняется в профиль.
|
||||
|
||||
## Модели данных (`club/models.py`)
|
||||
- `MembershipPlan`: `name`, `description`, `duration_days`, `price`, `created_at`.
|
||||
- `Profile`: `user` (OneToOne), `phone`, `created_at`.
|
||||
- `Subscription`: `user`, `plan`, `start_date`, `end_date`, `created_at`, вычисляемый `is_active`.
|
||||
- Миграция `0002_default_plans` создаёт 3 дефолтных тарифа (1, 3, 12 месяцев).
|
||||
- Контекстный список клубов в Ростове-на-Дону (5 адресов).
|
||||
|
||||
## Формы
|
||||
- `RegistrationForm` (расширенный `UserCreationForm`): username, имя, фамилия, телефон, email, пароль ×2; автосоздаёт `Profile`.
|
||||
|
|
|
|||
14
club/context_processors.py
Normal file
14
club/context_processors.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
CLUBS = [
|
||||
{"name": "Центр на Большой Садовой", "address": "ул. Большая Садовая, 105"},
|
||||
{"name": "Западный Арена", "address": "пр-т Стачки, 190"},
|
||||
{"name": "Северный Loft", "address": "пр-т Космонавтов, 14/1"},
|
||||
{"name": "Левенцовка Energy", "address": "ул. Малиновского, 25Г"},
|
||||
{"name": "Береговой RiverFit", "address": "Береговая ул., 15к3"},
|
||||
]
|
||||
|
||||
|
||||
def clubs_info(request):
|
||||
"""
|
||||
Делает список клубов доступным во всех шаблонах (шапка/футер/страницы).
|
||||
"""
|
||||
return {"clubs": CLUBS}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
|
|
@ -35,10 +37,21 @@ class RegistrationForm(BootstrapMixin, UserCreationForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._init_bootstrap()
|
||||
self.fields["phone"].widget.attrs.setdefault("placeholder", "+7XXXXXXXXXX")
|
||||
self.fields["phone"].widget.attrs.setdefault("inputmode", "tel")
|
||||
self.fields["username"].label = "Логин"
|
||||
for name in ["password1", "password2"]:
|
||||
self.fields[name].widget.attrs["autocomplete"] = "new-password"
|
||||
|
||||
def clean_phone(self):
|
||||
raw = self.cleaned_data.get("phone", "")
|
||||
digits = "".join(ch for ch in raw if ch.isdigit())
|
||||
if digits.startswith("8"):
|
||||
digits = "7" + digits[1:]
|
||||
if not digits.startswith("7") or len(digits) != 11:
|
||||
raise forms.ValidationError("Введите реальный номер в формате +7XXXXXXXXXX")
|
||||
return f"+{digits}"
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
user.first_name = self.cleaned_data.get("first_name", "")
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from .context_processors import CLUBS
|
||||
from .forms import LoginForm, RegistrationForm
|
||||
from .models import MembershipPlan, Subscription
|
||||
|
||||
|
||||
def home(request):
|
||||
plans = MembershipPlan.objects.all()
|
||||
return render(request, "home.html", {"plans": plans})
|
||||
return render(request, "home.html", {"plans": plans, "clubs": CLUBS})
|
||||
|
||||
|
||||
def register_view(request):
|
||||
|
|
|
|||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
|
@ -4,7 +4,7 @@ services:
|
|||
build: .
|
||||
command: /app/entrypoint.sh
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "1337:8000"
|
||||
environment:
|
||||
DJANGO_DEBUG: "True"
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ TEMPLATES = [
|
|||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"club.context_processors.clubs_info",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -58,7 +59,6 @@ DATABASES = {
|
|||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,27 @@ a:hover { color: var(--accent-strong); }
|
|||
.table > :not(caption) > * > * { background: transparent; color: var(--text); }
|
||||
.table { --bs-table-border-color: rgba(255,255,255,0.06); }
|
||||
|
||||
.form-control, .form-select { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: var(--text); }
|
||||
.form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2); }
|
||||
.form-control,
|
||||
.form-select {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--text);
|
||||
}
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2);
|
||||
}
|
||||
/* Читаемость выпадашек на всех платформах */
|
||||
.form-select,
|
||||
.form-select option {
|
||||
color: #0c1014;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.footer .form-select {
|
||||
background-color: #ffffff;
|
||||
color: #0c1014;
|
||||
}
|
||||
|
||||
.alert { border: 1px solid rgba(255,255,255,0.08); }
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,27 @@ a:hover { color: var(--accent-strong); }
|
|||
.table > :not(caption) > * > * { background: transparent; color: var(--text); }
|
||||
.table { --bs-table-border-color: rgba(255,255,255,0.06); }
|
||||
|
||||
.form-control, .form-select { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: var(--text); }
|
||||
.form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2); }
|
||||
.form-control,
|
||||
.form-select {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--text);
|
||||
}
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2);
|
||||
}
|
||||
/* Читаемость выпадашек на всех платформах */
|
||||
.form-select,
|
||||
.form-select option {
|
||||
color: #0c1014;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.footer .form-select {
|
||||
background-color: #ffffff;
|
||||
color: #0c1014;
|
||||
}
|
||||
|
||||
.alert { border: 1px solid rgba(255,255,255,0.08); }
|
||||
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@
|
|||
<div class="col-md-4">
|
||||
<select class="form-select form-select-lg" required>
|
||||
<option value="" selected disabled>Выберите клуб</option>
|
||||
<option>Центр</option>
|
||||
<option>Юг</option>
|
||||
<option>Север</option>
|
||||
{% for club in clubs %}
|
||||
<option>{{ club.name }} — {{ club.address }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 text-md-end">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-accent btn-lg" href="{% url 'register' %}">Зарегистрироваться</a>
|
||||
<a class="btn btn-outline-light btn-lg" href="#pricing">Выбрать абонемент</a>
|
||||
<a class="btn btn-light btn-lg" href="#booking">Записаться</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
|
|
@ -64,15 +65,99 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-4 py-lg-5" id="trainers">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Тренеры</h2>
|
||||
<span class="text-secondary small">Подберём специалиста под вашу цель</span>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 glass-card">
|
||||
<div class="card-body">
|
||||
<div class="fw-bold h5 mb-1">Алексей Кравцов</div>
|
||||
<div class="text-secondary small mb-2">Силовой тренинг · 8 лет стажа</div>
|
||||
<p class="text-secondary small mb-3">Составит программу на набор силы и массы, следит за техникой, специализируется на жиме и приседе.</p>
|
||||
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 glass-card">
|
||||
<div class="card-body">
|
||||
<div class="fw-bold h5 mb-1">Мария Иванова</div>
|
||||
<div class="text-secondary small mb-2">Функционал & кардио · 6 лет стажа</div>
|
||||
<p class="text-secondary small mb-3">Работает с похудением и общей выносливостью, включает TRX, HIIT и работу с собственным весом.</p>
|
||||
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 glass-card">
|
||||
<div class="card-body">
|
||||
<div class="fw-bold h5 mb-1">Игорь Петров</div>
|
||||
<div class="text-secondary small mb-2">Реабилитация · 10 лет стажа</div>
|
||||
<p class="text-secondary small mb-3">Помогает восстановиться после травм, аккуратно возвращает к нагрузкам, сотрудничает с врачами.</p>
|
||||
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-4 py-lg-5" id="locations">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="fw-bold">Клубы в Ростове-на-Дону</h2>
|
||||
<span class="text-secondary small">Все локации работают 24/7</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
{% for club in clubs %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100 glass-card">
|
||||
<div class="card-body">
|
||||
<div class="fw-bold mb-1">{{ club.name }}</div>
|
||||
<div class="text-secondary">{{ club.address }}</div>
|
||||
<div class="text-secondary small mt-2">Бесплатный первый визит по онлайн-заявке</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-4 py-lg-5" id="booking">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Записаться на тренировку</h2>
|
||||
<span class="text-secondary small">Первое занятие — бесплатно</span>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-7">
|
||||
<div class="card glass-card">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-secondary mb-4">Зарегистрируйтесь или войдите в личный кабинет, чтобы оформить абонемент и начать тренировки.</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="btn btn-accent btn-lg" href="{% url 'dashboard' %}">Перейти в кабинет</a>
|
||||
{% else %}
|
||||
<a class="btn btn-accent btn-lg" href="{% url 'register' %}">Зарегистрироваться</a>
|
||||
<a class="btn btn-outline-light btn-lg" href="{% url 'login' %}">Уже есть аккаунт</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-5" id="about">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h2 class="fw-bold">Команда тренеров</h2>
|
||||
<p class="text-secondary">Опытные специалисты по силовым, функциональному тренингу и реабилитации. Подберём программу под вашу цель — похудение, набор массы или поддержка здоровья.</p>
|
||||
<h2 class="fw-bold">О нас</h2>
|
||||
<p class="text-secondary">FitnessCenter — сеть клубов в Ростове-на-Дону с круглосуточным доступом, современной силовой зоной, кардио, залом функционального тренинга и комфортными раздевалками.</p>
|
||||
<ul class="list-unstyled text-secondary">
|
||||
<li class="mb-2">• Персональные и групповые тренировки</li>
|
||||
<li class="mb-2">• ДНК-тест и нутрициолог</li>
|
||||
<li class="mb-2">• Приложение с планом занятий</li>
|
||||
<li class="mb-2">• 24/7, электронный доступ по QR</li>
|
||||
<li class="mb-2">• Персоналки, групповые занятия, детская зона</li>
|
||||
<li class="mb-2">• Душ, полотенца, фитнес-бар с кофе и протеином</li>
|
||||
<li class="mb-2">• Онлайн-кабинет с абонементами и историей посещений</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
|
|
|||
Loading…
Reference in a new issue