347 lines
15 KiB
Python
347 lines
15 KiB
Python
from __future__ import annotations
|
||
from typing import List, Dict, Any, Optional
|
||
|
||
from django.http import Http404, HttpResponse
|
||
from django.shortcuts import render, redirect
|
||
from django.views.decorators.http import require_http_methods, require_POST
|
||
from django.contrib import messages
|
||
from django.core.exceptions import PermissionDenied
|
||
from django.urls import reverse, NoReverseMatch
|
||
|
||
from . import api
|
||
from .api import ApiError
|
||
|
||
|
||
# --- Reverse helper -----------------------------------------------------------
|
||
def _reverse_first(*names: str) -> str:
|
||
"""Return the first successfully reversed URL among candidates, else '/'."""
|
||
for n in names:
|
||
try:
|
||
return reverse(n)
|
||
except NoReverseMatch:
|
||
continue
|
||
# last resort:
|
||
try:
|
||
return reverse("ui:index")
|
||
except NoReverseMatch:
|
||
return "/"
|
||
|
||
def _is_logged(request) -> bool:
|
||
return bool(request.session.get("access_token") or request.COOKIES.get("access_token"))
|
||
|
||
def _is_admin(request) -> bool:
|
||
return (request.session.get("user_role") or "").upper() == "ADMIN"
|
||
|
||
def _set_tokens_and_user(request, tokens: Dict[str, Any], me: Dict[str, Any]):
|
||
request.session["access_token"] = tokens.get("access_token")
|
||
request.session["refresh_token"] = tokens.get("refresh_token")
|
||
request.session["user_id"] = me.get("id")
|
||
request.session["user_email"] = me.get("email")
|
||
request.session["user_full_name"] = me.get("full_name")
|
||
request.session["user_role"] = me.get("role")
|
||
request.session.modified = True
|
||
|
||
def _user_to_profile_stub(u: Dict[str, Any]) -> Dict[str, Any]:
|
||
name = u.get("full_name") or u.get("email") or "Без имени"
|
||
return {
|
||
"id": u.get("id"),
|
||
"name": name,
|
||
"email": u.get("email") or "",
|
||
"role": (u.get("role") or "").upper() or "CLIENT",
|
||
"verified": bool(u.get("is_active", False)),
|
||
"age": None,
|
||
"city": None,
|
||
"about": "",
|
||
"photo": "",
|
||
"interests": [],
|
||
"liked": False,
|
||
}
|
||
|
||
# === public pages ==============================================================
|
||
|
||
def index(request):
|
||
return render(request, "ui/index.html", {})
|
||
|
||
# === catalog (admin) ==========================================================
|
||
|
||
@require_http_methods(["GET"])
|
||
def profile_list(request):
|
||
if not _is_logged(request):
|
||
messages.info(request, "Войдите, чтобы открыть каталог")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
if not _is_admin(request):
|
||
messages.info(request, "Каталог доступен только администраторам. Перенаправляем в Кабинет.")
|
||
return redirect(_reverse_first("cabinet", "ui:cabinet"))
|
||
|
||
q = (request.GET.get("q") or "").strip().lower()
|
||
role = (request.GET.get("role") or "").strip().upper()
|
||
active = request.GET.get("active")
|
||
email_domain = (request.GET.get("domain") or "").strip().lower().lstrip("@")
|
||
sort = (request.GET.get("sort") or "name").strip().lower()
|
||
page = max(1, int(request.GET.get("page") or 1))
|
||
limit = min(max(1, int(request.GET.get("limit") or 20)), 200)
|
||
offset = (page - 1) * limit
|
||
|
||
error: Optional[str] = None
|
||
profiles: List[Dict[str, Any]] = []
|
||
page_info = {"page": page, "limit": limit, "has_prev": page > 1, "has_next": False}
|
||
|
||
try:
|
||
data = api.list_users(request, offset=offset, limit=limit)
|
||
users: List[Dict[str, Any]] = (data.get("items") if isinstance(data, dict) else data) or []
|
||
|
||
def keep(u: Dict[str, Any]) -> bool:
|
||
fn = (u.get("full_name") or "")
|
||
em = (u.get("email") or "")
|
||
if q and (q not in fn.lower() and q not in em.lower()):
|
||
return False
|
||
if role and (u.get("role") or "").upper() != role:
|
||
return False
|
||
if active in ("1", "0"):
|
||
is_act = bool(u.get("is_active", False))
|
||
if (active == "1" and not is_act) or (active == "0" and is_act):
|
||
return False
|
||
if email_domain:
|
||
dom = em.lower().split("@")[-1] if "@" in em else ""
|
||
if dom != email_domain:
|
||
return False
|
||
return True
|
||
|
||
users = [u for u in users if keep(u)]
|
||
|
||
def key_name(u): return (u.get("full_name") or u.get("email") or "").lower()
|
||
def key_email(u): return (u.get("email") or "").lower()
|
||
if sort == "name":
|
||
users.sort(key=key_name)
|
||
elif sort == "name_desc":
|
||
users.sort(key=key_name, reverse=True)
|
||
elif sort == "email":
|
||
users.sort(key=key_email)
|
||
elif sort == "email_desc":
|
||
users.sort(key=key_email, reverse=True)
|
||
|
||
profiles = [_user_to_profile_stub(u) for u in users]
|
||
liked_ids = set(request.session.get("likes", []))
|
||
for p in profiles:
|
||
p["liked"] = p.get("id") in liked_ids
|
||
|
||
page_info["has_next"] = (len(users) == limit and not (q or role or active in ("1","0") or email_domain))
|
||
|
||
except PermissionDenied as e:
|
||
messages.error(request, f"Нет доступа к каталогу: {e}")
|
||
return redirect(_reverse_first("cabinet", "ui:cabinet"))
|
||
except ApiError as e:
|
||
error = str(e)
|
||
|
||
ctx = {
|
||
"profiles": profiles,
|
||
"filters": {
|
||
"q": (request.GET.get("q") or "").strip(),
|
||
"role": role,
|
||
"active": (active or ""),
|
||
"domain": (request.GET.get("domain") or "").strip(),
|
||
"sort": sort,
|
||
"page": page,
|
||
"limit": limit,
|
||
},
|
||
"count": len(profiles),
|
||
"page": page_info,
|
||
"error": error,
|
||
"page_sizes": [10, 20, 50, 100, 200],
|
||
}
|
||
return render(request, "ui/profiles_list.html", ctx)
|
||
|
||
@require_http_methods(["GET"])
|
||
def profile_detail(request, pk):
|
||
try:
|
||
user = api.get_user(request, str(pk))
|
||
except PermissionDenied:
|
||
messages.error(request, "Сессия истекла, войдите снова")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
except ApiError as e:
|
||
if e.status == 404:
|
||
raise Http404("Пользователь не найден")
|
||
messages.error(request, str(e))
|
||
return redirect(_reverse_first("profiles", "ui:profiles"))
|
||
|
||
stub = _user_to_profile_stub(user)
|
||
liked_ids = set(request.session.get("likes", []))
|
||
liked = stub["id"] in liked_ids
|
||
return render(request, "ui/profile_detail.html", {"profile": stub, "liked": liked})
|
||
|
||
@require_POST
|
||
def like_profile(request, pk):
|
||
likes = set(request.session.get("likes", []))
|
||
pk_str = str(pk)
|
||
if pk_str in likes:
|
||
likes.remove(pk_str); liked = False
|
||
else:
|
||
likes.add(pk_str); liked = True
|
||
request.session["likes"] = list(likes)
|
||
request.session.modified = True
|
||
return render(request, "ui/components/like_button.html", {"profile_id": pk_str, "liked": liked})
|
||
|
||
# === auth =====================================================================
|
||
|
||
@require_http_methods(["GET", "POST"])
|
||
def login_view(request):
|
||
if request.method == "POST":
|
||
email = (request.POST.get("email") or "").strip()
|
||
password = (request.POST.get("password") or "").strip()
|
||
if not email or not password:
|
||
messages.error(request, "Укажите email и пароль")
|
||
else:
|
||
try:
|
||
tokens = api.login(request, email, password)
|
||
me = api.get_me(request)
|
||
_set_tokens_and_user(request, tokens, me)
|
||
|
||
next_url = request.GET.get("next")
|
||
if not next_url:
|
||
if (me.get("role") or "").upper() == "ADMIN":
|
||
next_url = _reverse_first("profiles", "ui:profiles")
|
||
else:
|
||
next_url = _reverse_first("cabinet", "ui:cabinet")
|
||
|
||
resp = redirect(next_url)
|
||
max_age = 7 * 24 * 3600
|
||
resp.set_cookie("access_token", tokens.get("access_token"), max_age=max_age, httponly=True, samesite="Lax")
|
||
resp.set_cookie("refresh_token", tokens.get("refresh_token"), max_age=max_age, httponly=True, samesite="Lax")
|
||
messages.success(request, "Вы успешно вошли")
|
||
return resp
|
||
except PermissionDenied as e:
|
||
messages.error(request, f"Доступ запрещён: {e}")
|
||
except ApiError as e:
|
||
messages.error(request, f"Ошибка входа: {e.args[0]}")
|
||
return render(request, "ui/login.html", {})
|
||
|
||
@require_http_methods(["GET", "POST"])
|
||
def register_view(request):
|
||
if request.method == "POST":
|
||
email = (request.POST.get("email") or "").strip()
|
||
password = (request.POST.get("password") or "").strip()
|
||
full_name = (request.POST.get("full_name") or "").strip() or None
|
||
if not email or not password:
|
||
messages.error(request, "Укажите email и пароль")
|
||
else:
|
||
try:
|
||
api.register_user(request, email, password, full_name, role="CLIENT")
|
||
tokens = api.login(request, email, password)
|
||
me = api.get_me(request)
|
||
_set_tokens_and_user(request, tokens, me)
|
||
resp = redirect(_reverse_first("cabinet", "ui:cabinet"))
|
||
max_age = 7 * 24 * 3600
|
||
resp.set_cookie("access_token", tokens.get("access_token"), max_age=max_age, httponly=True, samesite="Lax")
|
||
resp.set_cookie("refresh_token", tokens.get("refresh_token"), max_age=max_age, httponly=True, samesite="Lax")
|
||
messages.success(request, "Регистрация успешна")
|
||
return resp
|
||
except ApiError as e:
|
||
messages.error(request, f"Ошибка регистрации: {e.args[0]}")
|
||
return render(request, "ui/register.html", {})
|
||
|
||
@require_http_methods(["POST", "GET"])
|
||
def logout_view(request):
|
||
resp = redirect(_reverse_first("index", "ui:index"))
|
||
resp.delete_cookie("access_token")
|
||
resp.delete_cookie("refresh_token")
|
||
for k in ("auth", "access_token", "refresh_token", "user_id", "user_email", "user_full_name", "user_role"):
|
||
request.session.pop(k, None)
|
||
request.session.modified = True
|
||
messages.info(request, "Вы вышли из аккаунта")
|
||
return resp
|
||
|
||
# === cabinet ==================================================================
|
||
|
||
@require_http_methods(["GET", "POST"])
|
||
def cabinet_view(request):
|
||
if not _is_logged(request):
|
||
messages.info(request, "Войдите, чтобы открыть кабинет")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
|
||
profile = None
|
||
has_profile = False
|
||
try:
|
||
profile = api.get_my_profile(request)
|
||
has_profile = bool(profile)
|
||
except PermissionDenied:
|
||
messages.error(request, "Сессия истекла, войдите снова")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
except ApiError as e:
|
||
if e.status != 404:
|
||
messages.error(request, f"Ошибка чтения профиля: {e}")
|
||
|
||
if request.method == "POST":
|
||
action = (request.POST.get("action") or "").strip()
|
||
try:
|
||
if action == "create_profile":
|
||
gender = (request.POST.get("gender") or "").strip()
|
||
city = (request.POST.get("city") or "").strip()
|
||
languages = [s.strip() for s in (request.POST.get("languages") or "").split(",") if s.strip()]
|
||
interests = [s.strip() for s in (request.POST.get("interests") or "").split(",") if s.strip()]
|
||
api.create_my_profile(request, gender, city, languages, interests)
|
||
messages.success(request, "Профиль создан")
|
||
return redirect(_reverse_first("cabinet", "ui:cabinet"))
|
||
elif action == "update_name":
|
||
full_name = (request.POST.get("full_name") or "").strip()
|
||
if not full_name:
|
||
messages.error(request, "Имя не может быть пустым")
|
||
else:
|
||
api.update_user_me(request, request.session.get("user_id"), full_name=full_name)
|
||
request.session["user_full_name"] = full_name
|
||
request.session.modified = True
|
||
messages.success(request, "Имя обновлено")
|
||
return redirect(_reverse_first("cabinet", "ui:cabinet"))
|
||
except PermissionDenied:
|
||
messages.error(request, "Сессия истекла, войдите снова")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
except ApiError as e:
|
||
messages.error(request, f"Ошибка: {e}")
|
||
|
||
ctx = {
|
||
"has_profile": has_profile,
|
||
"profile": profile,
|
||
}
|
||
return render(request, "ui/cabinet.html", ctx)
|
||
|
||
@require_POST
|
||
def cabinet_upload_photo(request):
|
||
# Требуем авторизацию
|
||
if not (request.COOKIES.get('access_token') or request.session.get('access_token') or (request.session.get('auth') or {}).get('access_token')):
|
||
messages.error(request, "Сначала войдите")
|
||
return redirect('ui:login')
|
||
|
||
file_obj = request.FILES.get('file') or request.FILES.get('photo')
|
||
if not file_obj:
|
||
messages.error(request, "Не выбрано фото")
|
||
return redirect('ui:cabinet')
|
||
|
||
try:
|
||
prof = api.upload_my_profile_photo(request, file_obj)
|
||
# Обновлённый профиль приходит от API (в т.ч. photo_url)
|
||
messages.success(request, "Фото успешно обновлено")
|
||
except PermissionDenied:
|
||
messages.error(request, "Сессия истекла, войдите снова")
|
||
return redirect('ui:login')
|
||
except ApiError as e:
|
||
messages.error(request, f"Не удалось загрузить фото: {e.payload.get('detail') or e.args[0]}")
|
||
|
||
return redirect('ui:cabinet')
|
||
|
||
@require_POST
|
||
def cabinet_delete_photo(request):
|
||
if not _is_logged(request):
|
||
messages.info(request, "Войдите, чтобы удалить фото")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
try:
|
||
api.delete_my_photo(request)
|
||
messages.success(request, "Фото удалено")
|
||
except PermissionDenied:
|
||
messages.error(request, "Сессия истекла, войдите снова")
|
||
return redirect(_reverse_first("login", "ui:login"))
|
||
except ApiError as e:
|
||
if e.status in (404, 405):
|
||
messages.error(request, "Удаление фото не поддерживается бэкендом.")
|
||
else:
|
||
messages.error(request, f"Ошибка удаления: {e}")
|
||
return redirect(_reverse_first("cabinet", "ui:cabinet"))
|