avtopost.kultmost.ru/main.ipynb
2026-08-30 23:18:26 -04:00

5136 lines
200 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "4e3a145b",
"metadata": {},
"source": [
"# FastAPI"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "44794e2f",
"metadata": {},
"outputs": [],
"source": [
"# ignore\n",
"import os\n",
"\n",
"def load_env(file_path):\n",
" \"\"\"Загружает переменные окружения из файла .env с поддержкой булевых значений.\"\"\"\n",
" with open(file_path) as f:\n",
" for line in f:\n",
" line = line.strip()\n",
" if not line or line.startswith('#'):\n",
" continue\n",
" \n",
" try:\n",
" key, value = line.split('=', 1)\n",
" key = key.strip()\n",
" value = value.strip('\"\\' ')\n",
" # Преобразование строковых значений в булевые\n",
" if value.lower() in ['true', '1', 'yes']:\n",
" value = True\n",
" elif value.lower() in ['false', '0', 'no']:\n",
" value = False\n",
" \n",
" # Проверка, что ключ не пустой\n",
" if key:\n",
" os.environ[key] = str(value) # Храним как строку\n",
" except ValueError:\n",
" print(f\"Ошибка: неверный формат в строке '{line}'\")\n",
"\n",
"try:\n",
" load_env('env')\n",
"except:\n",
" load_env('env_example')"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "dd08738b",
"metadata": {},
"source": [
"## App"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "49f00c66",
"metadata": {},
"outputs": [],
"source": [
"from fastapi import FastAPI, APIRouter, UploadFile, File, Depends, HTTPException, Body, Security, Form, Request, Response, BackgroundTasks, Query\n",
"from motor.motor_asyncio import AsyncIOMotorCollection\n",
"import jwt\n",
"from datetime import datetime, timedelta, timezone, date\n",
"import io\n",
"import asyncio\n",
"from fastapi.middleware.cors import CORSMiddleware\n",
"from fastapi.responses import HTMLResponse, StreamingResponse, RedirectResponse\n",
"from fastapi.staticfiles import StaticFiles\n",
"import mimetypes\n",
"from urllib.parse import quote\n",
"from starlette.middleware.gzip import GZipMiddleware\n",
"import uuid\n",
"import bcrypt\n",
"from typing import List, Any, Dict, Optional, Union\n",
"from datetime import timedelta\n",
"from urllib.parse import urlencode\n",
"from bson import ObjectId\n",
"from PIL import Image\n",
"from io import BytesIO\n",
"from dateutil.parser import isoparse\n",
"import httpx\n",
"import string\n",
"from db import *\n",
"import vk as vk\n",
"import tg as tg\n",
"from delivery_router import router as delivery_router\n",
"from auth import get_user, get_current_user, get_bearer_token, get_access_token, get_token\n",
"from roles import *\n",
"from utils import *\n",
"from tasks import *\n",
"from models import * \n",
"from config import *\n",
"import logging\n",
"import json\n",
"from templates import templates\n",
"import email_task as email_task\n",
"from pydantic import EmailStr\n",
"import secrets\n",
"from pymongo import ReturnDocument\n",
"\n",
"logging.basicConfig(level=logging.INFO)\n",
"logger = logging.getLogger(__name__)"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "28509a4b",
"metadata": {},
"source": [
"### Конфигурация"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0142add9",
"metadata": {},
"outputs": [],
"source": [
"app = FastAPI(title=\"KULTMOST\")\n",
"\n",
"app.mount(\"/static\", StaticFiles(directory=\"static\"), name=\"static\")\n",
"app.add_middleware(GZipMiddleware, minimum_size=500)\n",
"app.add_middleware(\n",
" CORSMiddleware,\n",
" allow_origins=['*'],\n",
" allow_credentials=True,\n",
" allow_methods=[\"*\"],\n",
" allow_headers=[\"*\"],\n",
")\n",
"app.include_router(delivery_router)\n",
"app.include_router(vk.router, prefix='/api/v1/social', tags=[\"Auth\"])"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "f299b21b",
"metadata": {},
"source": [
"### Хелперы"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "77d90b86",
"metadata": {},
"outputs": [],
"source": [
"RUS_TO_LAT = {\n",
" 'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z',\n",
" 'и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r',\n",
" 'с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'shch',\n",
" 'ъ':'\"','ы':'y','ь':\"'\",'э':'e','ю':'yu','я':'ya',\n",
" ' ':'-',\n",
" 'А':'A','Б':'B','В':'V','Г':'G','Д':'D','Е':'E','Ё':'Yo','Ж':'Zh','З':'Z',\n",
" 'И':'I','Й':'Y','К':'K','Л':'L','М':'M','Н':'N','О':'O','П':'P','Р':'R',\n",
" 'С':'S','Т':'T','У':'U','Ф':'F','Х':'Kh','Ц':'Ts','Ч':'Ch','Ш':'Sh','Щ':'Shch',\n",
" 'Ъ':'\"','Ы':'Y','Ь':\"'\",'Э':'E','Ю':'Yu','Я':'Ya'\n",
"}\n",
"\n",
"def transliterate(text: str) -> str:\n",
" return ''.join(RUS_TO_LAT.get(ch, '' if ch not in string.ascii_letters else ch) for ch in text)\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "77d90b86",
"metadata": {},
"outputs": [],
"source": [
"async def get_latest_news() -> List[dict]:\n",
" cursor = news_collection.find({'publish_on_platform': True}).sort(\"_id\", -1).limit(30)\n",
" docs: List[dict] = []\n",
" async for doc in cursor:\n",
" published = doc.get(\"published_at\")\n",
" if published is not None and hasattr(published, \"isoformat\"):\n",
" doc[\"published_at\"] = published.isoformat()\n",
" docs.append(doc)\n",
" return docs\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "1f960469",
"metadata": {},
"outputs": [],
"source": [
"async def logo_user_task(user: dict):\n",
" img_id = user.get('img') if user else None\n",
" new_id = None\n",
"\n",
" if img_id:\n",
" # download original image bytes\n",
" file_data = await gridfs_map['users'].open_download_stream(ObjectId(img_id))\n",
" original_image_data = await file_data.read()\n",
"\n",
" # store original in GridFS (keep a copy)\n",
" original_img_id = await gridfs_map['users'].upload_from_stream(f'original_{img_id}', BytesIO(original_image_data))\n",
"\n",
" # open image with PIL\n",
" img = Image.open(BytesIO(original_image_data))\n",
" w, h = img.size\n",
"\n",
" # resize if needed\n",
" if w > 500:\n",
" factor = 500 / w\n",
" new_size = (500, int(h * factor))\n",
" img = img.resize(new_size, resample=Image.LANCZOS)\n",
"\n",
" # ensure image mode is compatible with JPEG\n",
" if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):\n",
" # composite onto white background to remove alpha\n",
" background = Image.new('RGB', img.size, (255, 255, 255))\n",
" alpha = img.split()[-1] if img.mode != 'P' else img.convert('RGBA').split()[-1]\n",
" background.paste(img.convert('RGBA'), mask=alpha)\n",
" img = background\n",
" else:\n",
" img = img.convert('RGB')\n",
"\n",
" # save to buffer as JPEG\n",
" buf = BytesIO()\n",
" # img.save(buf, format='JPEG', quality=85)\n",
" img.save(buf, format=\"WEBP\", quality=85, method=6)\n",
" buf.seek(0)\n",
"\n",
" # upload processed avatar to GridFS\n",
" new_id = await gridfs_map['users'].upload_from_stream('500_avatar.jpg', buf)\n",
"\n",
" # update user doc with new image ids (store as strings to match your original code)\n",
" await users_collection.update_one(\n",
" {'_id': ObjectId(user.get('_id'))},\n",
" {'$set': {'img': str(new_id), 'original_img': str(original_img_id)}}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a666b69",
"metadata": {},
"outputs": [],
"source": [
"async def poster_competition(competition):\n",
" img_id = competition.get('poster_orig_id')\n",
" try:\n",
" stream = await gridfs_map['competition'].open_download_stream(ObjectId(img_id))\n",
" data = await stream.read()\n",
"\n",
" buf = await build_composite(data, 0.75, 800)\n",
"\n",
" new_id = await gridfs_map['competition'].upload_from_stream(f\"500_{stream.filename}\", buf)\n",
"\n",
" except Exception:\n",
" new_id = img_id\n",
" print(f'Ошибка обработки id {img_id} в выставке {competition[\"_id\"]}')\n",
"\n",
" if new_id:\n",
" await competitions_col.update_one(\n",
" {'_id': ObjectId(competition.get('_id'))},\n",
" {'$set': {\n",
" 'poster_file_id': new_id\n",
" }}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "185ed4e5",
"metadata": {},
"outputs": [],
"source": [
"async def poster_exhibition(exhibition):\n",
" img_id = exhibition.get('poster_orig_id')\n",
" try:\n",
" stream = await gridfs_map['exhibition'].open_download_stream(ObjectId(img_id))\n",
" data = await stream.read()\n",
"\n",
" buf = await build_composite(data, 0.73, 800)\n",
"\n",
" new_id = await gridfs_map['exhibition'].upload_from_stream(f\"500_{stream.filename}\", buf)\n",
"\n",
" except Exception:\n",
" new_id = img_id\n",
" print(f'Ошибка обработки id {img_id} в выставке {exhibition[\"_id\"]}')\n",
"\n",
" if new_id:\n",
" await exhibition_collection.update_one(\n",
" {'_id': ObjectId(exhibition.get('_id'))},\n",
" {'$set': {\n",
" 'poster_file_id': new_id\n",
" }}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d00fe86",
"metadata": {},
"outputs": [],
"source": [
"async def replace_new_medias(post, links_media):\n",
" img_ids = post.get('medias_original', [])\n",
" if not isinstance(img_ids, list):\n",
" img_ids = [img_ids]\n",
"\n",
" new_ids = []\n",
"\n",
" for img_id in img_ids:\n",
" if img_id in links_media:\n",
" new_ids.append(links_media['img_id'])\n",
" continue\n",
" try:\n",
" stream = await gridfs_map['posts'].open_download_stream(ObjectId(img_id))\n",
" data = await stream.read()\n",
"\n",
" buf = await build_composite(data)\n",
"\n",
" new_id = await gridfs_map['posts'].upload_from_stream(f\"500_{stream.filename}\", buf)\n",
" new_ids.append(str(new_id))\n",
"\n",
" except Exception:\n",
" new_ids.append(str(img_id))\n",
" print(f'Ошибка обработки id {img_id} в посте {post[\"_id\"]}')\n",
"\n",
" if new_ids:\n",
" await posts_collection.update_one(\n",
" {'_id': ObjectId(post.get('_id'))},\n",
" {'$set': {\n",
" 'medias': new_ids\n",
" }}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "80790203",
"metadata": {},
"outputs": [],
"source": [
"async def replace_post_medias(post):\n",
" img_ids = post.get('medias_original', [])\n",
" if not isinstance(img_ids, list):\n",
" img_ids = [img_ids]\n",
"\n",
" new_ids = []\n",
"\n",
" for img_id in img_ids:\n",
" try:\n",
" stream = await gridfs_map['posts'].open_download_stream(ObjectId(img_id))\n",
" data = await stream.read()\n",
"\n",
" buf = await build_composite(data)\n",
"\n",
" new_id = await gridfs_map['posts'].upload_from_stream(f\"500_{stream.filename}\", buf)\n",
" new_ids.append(str(new_id))\n",
"\n",
" except Exception:\n",
" new_ids.append(str(img_id))\n",
" print(f'Ошибка обработки id {img_id} в посте {post[\"_id\"]}')\n",
"\n",
" if new_ids:\n",
" await posts_collection.update_one(\n",
" {'_id': ObjectId(post.get('_id'))},\n",
" {'$set': {\n",
" 'medias': new_ids\n",
" }}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d512558a",
"metadata": {},
"outputs": [],
"source": [
"async def replace_news_medias(news):\n",
" img_ids = news.get('orig_media', [])\n",
" if not isinstance(img_ids, list):\n",
" img_ids = [img_ids]\n",
"\n",
" new_ids = []\n",
"\n",
" for img_id in img_ids:\n",
" try:\n",
" stream = await gridfs_map['news'].open_download_stream(ObjectId(img_id))\n",
" data = await stream.read()\n",
"\n",
" buf = await build_composite(data)\n",
"\n",
" new_id = await gridfs_map['news'].upload_from_stream(f\"500_{stream.filename}\", buf)\n",
" new_ids.append(str(new_id))\n",
"\n",
" except Exception:\n",
" new_ids.append(str(img_id))\n",
" print(f'Ошибка обработки id {img_id} в посте {news[\"_id\"]}')\n",
"\n",
" if new_ids:\n",
" await news_collection.update_one(\n",
" {'_id': ObjectId(news.get('_id'))},\n",
" {'$set': {\n",
" 'media': new_ids\n",
" }}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "02d14e84",
"metadata": {},
"outputs": [],
"source": [
"async def save_to_gridfs(upload: UploadFile, bucket_name: str):\n",
" if upload is None:\n",
" return None\n",
"\n",
" grid = gridfs_map.get(bucket_name)\n",
" if grid is None:\n",
" return None\n",
"\n",
" filename = upload.filename or str(uuid.uuid4())\n",
"\n",
" contents = await upload.read()\n",
" stream = io.BytesIO(contents)\n",
"\n",
" try:\n",
" file_id = await grid.upload_from_stream(filename, stream)\n",
" return str(file_id)\n",
" finally:\n",
" await upload.close()\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "3e085813",
"metadata": {},
"outputs": [],
"source": [
"async def posts_temp(user,request):\n",
" data=[]\n",
" async for i in posts_collection.find({'user._id': user.get('_id')}).sort('_id',-1).limit(1000):\n",
" data.append(i)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/posts.html\", context={ \"user\": user, 'cards': data}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "abe0b1c9",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/cron/day\")\n",
"async def cron_every_day(token: str = Security(get_token)):\n",
" res = await competitions_col.update_many(\n",
" {\n",
" \"date_finish\": {\"$lt\": datetime.now(timezone.utc) + timedelta(hours=3)},\n",
" \"status\": {\"$lt\": 2},\n",
" },\n",
" {\"$set\": {\"status\": 2}},\n",
" )\n",
" return {}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d49bb0b6",
"metadata": {},
"outputs": [],
"source": [
"# await notify_collection.delete_many({'title':'Доставка картины'})"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "f9a0e519",
"metadata": {},
"outputs": [],
"source": [
"# await notify_collection.update_many({'title':'Доставка картины'},{'$set':{'type':'delivery'}})"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "43cfd9a2",
"metadata": {},
"outputs": [],
"source": [
"# async for existing in delivery_collection.find({\n",
"# 'exhibition_id':\n",
"# '6a55f0e90154435ba37a7c25'\n",
"# ,\n",
"# 'status':\n",
"# 'Ожидает получения'\n",
"# }):\n",
"# print(str(existing['receiver'].get('_id')))\n",
"# notify = await notify_in_profile(NotifyItem(\n",
"# user_id=str(existing['receiver'].get('_id')),\n",
"# type_item='delivery',\n",
"# title='Доставка картины',\n",
"# link=f'/profile/delivery',\n",
"# text=f'{str(existing['sender']['username'])} прислал вам картину — {existing['title']}'\n",
"# ))\n",
"# print(notify)\n",
"# await delivery_collection.update_one({'_id': existing.get('_id')},{'$set':{'notify': notify.upserted_id}})"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "95af5bfa",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/cron/5\")\n",
"async def cron_every_five_min(token: str = Security(get_token)):\n",
" now = datetime.now(timezone.utc)\n",
" limit_dt = now + timedelta(hours=3)\n",
"\n",
" prod_filter = {\"prod\": PROD}\n",
"\n",
" # 1) If there's an exhibition that is going to be finished soon -> move deliveries to \"Возврат художнику\"\n",
" exhibition_finished = await exhibition_collection.find_one_and_update(\n",
" {**prod_filter, \"status\": 1, \"date_finish\": {\"$lt\": limit_dt}},\n",
" {\"$set\": {\"status\": 2}},\n",
" return_document=ReturnDocument.AFTER,\n",
" )\n",
" if exhibition_finished:\n",
" exhibition_id_str = str(exhibition_finished[\"_id\"])\n",
"\n",
" async for doc in delivery_collection.find(\n",
" {\"exhibition_id\": exhibition_id_str, \"status\": \"Получено\", **prod_filter},\n",
" {\"_id\": 0},\n",
" ):\n",
" sender = doc.get(\"sender\")\n",
" post_id = doc.get(\"post_id\")\n",
" exhibition_id = doc.get(\"exhibition_id\")\n",
"\n",
" new_doc = {\n",
" **doc,\n",
" \"delivery_type\": \"\",\n",
" \"track\": \"\",\n",
" \"status\": \"Возврат художнику\",\n",
" \"receiver\": sender,\n",
" \"sender\": doc.get(\"receiver\"),\n",
" }\n",
"\n",
" await delivery_collection.update_one(\n",
" {**prod_filter, \"status\": \"Возврат художнику\", \"post_id\": post_id, \"exhibition_id\": exhibition_id},\n",
" {\"$set\": new_doc},\n",
" upsert=True,\n",
" )\n",
"\n",
" return {}\n",
" tasks = []\n",
" async for exhibition_to_start in exhibition_collection.find(\n",
" {**prod_filter}\n",
" ):\n",
" exhibition_id_str = str(exhibition_to_start[\"_id\"])\n",
" exhibition_title = exhibition_to_start.get(\"title\")\n",
"\n",
" notified_user_id = None\n",
" notified_post_user_id = None\n",
"\n",
" async for doc in delivery_collection.find(\n",
" {\"exhibition_id\": exhibition_id_str, \"status\": \"Получено\", **prod_filter},\n",
" {\"_id\": 0},\n",
" ):\n",
" post_id = doc.get(\"post_id\")\n",
" exhibition_id = doc.get(\"exhibition_id\")\n",
"\n",
" post = await posts_collection.find_one({\"_id\": ObjectId(post_id)})\n",
"\n",
" # Use user from post; keep them even if multiple deliveries exist\n",
" if post and post.get(\"user\") and not notified_post_user_id:\n",
" notified_post_user_id = str(post[\"user\"].get(\"_id\"))\n",
"\n",
" new_doc = {\n",
" **doc,\n",
" \"exhibition\": exhibition_to_start,\n",
" \"status\": \"На выставке\",\n",
" \"user\": post.get(\"user\") if post else None,\n",
" \"post\": post,\n",
" **prod_filter\n",
" }\n",
"\n",
" tasks.append(delivery_collection.update_one(\n",
" {**prod_filter, \"status\": \"На выставке\", \"post_id\": post_id, \"exhibition_id\": exhibition_id},\n",
" {\"$set\": new_doc},\n",
" upsert=True,\n",
" ))\n",
" tasks.append(posts_collection.update_one(\n",
" {\"_id\": ObjectId(post_id)},\n",
" {\"$set\": {\"status\": \"На выставке\"}}\n",
" ))\n",
"\n",
" # NOTE: your original code used `post` from inside the loop (could be last item / even undefined).\n",
" # This version notifies once using the first found post user.\n",
" if notified_post_user_id:\n",
" await notify_in_profile(\n",
" NotifyItem(\n",
" user_id=notified_post_user_id,\n",
" type_item=\"exhibtion\",\n",
" title=f\"Выставка {exhibition_title}\",\n",
" link=\"/profile/exhibition\",\n",
" text=\"Ваши картины на выставке\",\n",
" )\n",
" )\n",
" if tasks:\n",
" asyncio.gather(*tasks)\n",
" # 2) Otherwise find an exhibition that should start soon -> move deliveries to \"На выставке\"\n",
" exhibition_to_start = await exhibition_collection.find_one_and_update(\n",
" {**prod_filter, \"status\": 0, \"datetime_start\": {\"$lt\": limit_dt}},\n",
" {\"$set\": {\"status\": 1}},\n",
" return_document=ReturnDocument.AFTER,\n",
" )\n",
"\n",
" if not exhibition_to_start:\n",
" return {}\n",
"\n",
" # If vk/tg arrays are empty -> send social invitation\n",
" if exhibition_to_start.get(\"vk\") == [] or exhibition_to_start.get(\"tg\") == []:\n",
" await service_social_exhibition(bson_clear(exhibition_to_start))\n",
" return {}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "517caafe",
"metadata": {},
"outputs": [],
"source": [
"# ignore\n",
"# exhibition = await exhibition_collection.find_one_and_update(\n",
"# {\"prod\": True, \"datetime_start\": {\"$lt\": datetime.now(timezone.utc) + timedelta(hours=3)}},\n",
"# {\"$set\": {\"status\": 1}}, return_document=False)\n",
"# await service_social_exhibition(exhibition)\n"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "3e085813",
"metadata": {},
"source": [
"### Эндпоинты"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8faed7c5",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/register\", response_model=dict)\n",
"async def register(\n",
" background_tasks: BackgroundTasks,\n",
" username: Optional[str] = Form(None),\n",
" phone: Optional[str] = Form(None),\n",
" email: Optional[str] = Form(None),\n",
" password: str = Form(...),\n",
" client_id: str = Form(...),\n",
" roles: List[str] = Form(...),\n",
" birthday: Optional[date] = Form(None),\n",
" grade: Optional[str] = Form(None),\n",
" Female_Name_Lastame: Optional[str] = Form(None),\n",
" living_place: Optional[str] = Form(None),\n",
" offer_accepted: Optional[bool] = Form(False),\n",
" img: Optional[UploadFile] = File(None),\n",
" request: Request = None,\n",
" response: Response = None\n",
"):\n",
" user = UserRegister(\n",
" username=username, phone=phone, email=email, password=password,\n",
" client_id=client_id, roles=roles, birthday=birthday, grade=grade,\n",
" Female_Name_Lastame=Female_Name_Lastame, living_place=living_place,\n",
" offer_accepted=offer_accepted\n",
" )\n",
" # if normalize_phone(user.phone) and await users_collection.find_one({\"phone\": normalize_phone(user.phone)}):\n",
" # raise HTTPException(400, \"Телефон уже зарегестрирован\")\n",
" if user.email and await users_collection.find_one({\"email\": user.email}):\n",
" raise HTTPException(400, \"Email уже зарегестрирован\")\n",
"\n",
" hashed_password = get_password_hash(user.password)\n",
" allowed_roles = [r for r in user.roles if r not in BLOCK_ROLES]\n",
" if any(r in REQUIRES_OFFER for r in user.roles):\n",
" if not user.offer_accepted:\n",
" raise HTTPException(status_code=400, detail=\"Оферта должна быть принята\")\n",
" if not user.offer_accepted_at:\n",
" user.offer_accepted_at = datetime.utcnow()\n",
" if img:\n",
" img_id = await save_to_gridfs(img, 'users')\n",
" else:\n",
" img_id = '/static/painter.svg'\n",
"\n",
" doc = {\n",
" \"username\": user.username,\n",
" \"phone\": normalize_phone(user.phone) if user.phone else None,\n",
" \"email\": user.email,\n",
" \"img\": img_id,\n",
" \"hashed_password\": hashed_password,\n",
" \"roles\": allowed_roles,\n",
" \"birthday\": user.birthday.isoformat() if user.birthday else None,\n",
" \"grade\": user.grade,\n",
" \"female_name_lastname\": user.female_name_lastname,\n",
" \"living_place\": user.living_place,\n",
" \"client_id\": user.client_id,\n",
" \"extra_scopes\": user.extra_scopes,\n",
" \"offer_accepted\": bool(user.offer_accepted),\n",
" \"offer_accepted_at\": user.offer_accepted_at.isoformat() if user.offer_accepted_at else None,\n",
" }\n",
"\n",
" result = await users_collection.insert_one(doc)\n",
" user_id_str = str(result.inserted_id)\n",
" \n",
" doc_out = {\n",
" \"_id\": user_id_str,\n",
" \"id\": user_id_str,\n",
" **{k: v for k, v in doc.items()}\n",
" }\n",
" \n",
" background_tasks.add_task(email_new_user, bson_clear(doc_out))\n",
" background_tasks.add_task(logo_user_task, bson_clear(doc_out))\n",
" scopes = get_all_user_scopes(allowed_roles, [])\n",
"\n",
" client_id = user.client_id\n",
" client_ip = request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" access_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": client_id, \"ip\": client_ip},\n",
" access_exp_delta\n",
" )\n",
" refresh_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": client_id, \"ip\": client_ip},\n",
" refresh_exp_delta\n",
" )\n",
"\n",
" await redis_client.setex(f\"access:{access_token}\", int(access_exp_delta.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{hash_token(refresh_token)}\", int(refresh_exp_delta.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{access_token}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{hash_token(refresh_token)}\")\n",
"\n",
"\n",
"\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
"\n",
" return {\n",
" \"ok\": True,\n",
" \"access_token\": access_token,\n",
" \"refresh_token\": refresh_token,\n",
" \"user_id\": user_id_str,\n",
" \"token_type\": \"bearer\"\n",
" }\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "8faed7c5",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/admin/login\")\n",
"async def login_admin(\n",
" request: Request,\n",
" response: Response,\n",
" login_data: AdminLoginRequest = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"admin\"])\n",
"):\n",
" user = await users_collection.find_one({\"_id\": ObjectId(login_data.user_id)})\n",
" user_id_str = str(user[\"_id\"])\n",
" scopes = get_all_user_scopes(user.get(\"roles\", []), user.get(\"extra_scopes\", []))\n",
"\n",
" client_ip = request.headers.get('X-Real-IP') or request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" access_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": 'admin', \"ip\": client_ip},\n",
" access_exp_delta\n",
" )\n",
" refresh_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": 'admin', \"ip\": client_ip},\n",
" refresh_exp_delta\n",
" )\n",
"\n",
" await redis_client.setex(f\"access:{access_token}\", int(access_exp_delta.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{hash_token(refresh_token)}\", int(refresh_exp_delta.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{access_token}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{hash_token(refresh_token)}\")\n",
"\n",
"\n",
" admin_access_token = request.cookies.get(\"access_token\")\n",
" admin_refresh_token = request.cookies.get(\"refresh_token\")\n",
" response.set_cookie(\n",
" key=\"admin_access_token\",\n",
" value=admin_access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"admin_refresh_token\",\n",
" value=admin_refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
"\n",
" return {\n",
" \"access_token\": access_token,\n",
" \"refresh_token\": refresh_token,\n",
" \"user_id\": user_id_str,\n",
" \"token_type\": \"bearer\"\n",
" }\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "092e6b9a",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/admin/back\")\n",
"async def login_admin_back(\n",
" request: Request,\n",
" response: Response,\n",
"):\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
" admin_access_token = request.cookies.get(\"admin_access_token\")\n",
" admin_refresh_token = request.cookies.get(\"refreadmin_refresh_tokensh_token\")\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=admin_access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=admin_refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "b7ddd254",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/login/{token}\")\n",
"async def login_token(token: str, request: Request, response: Response):\n",
" token_hash = hash_token(token)\n",
"\n",
" log = await users_token_collection.find_one(\n",
" {'token_hash': token_hash}\n",
" )\n",
" if not log:\n",
" return RedirectResponse(\"/login?\" + urlencode({\"next\": '/profile'}))\n",
" next_url = log.get('url', '/')\n",
" created = log.get(\"created_at\")\n",
" if not created:\n",
" try:\n",
" created = isoparse(ObjectId(log.get(\"_id\")).generation_time.replace(tzinfo=timezone.utc))\n",
" except Exception:\n",
" return RedirectResponse(\"/login?\" + urlencode({\"next\": next_url}))\n",
" \n",
" created = created.replace(tzinfo=timezone.utc)\n",
" now = datetime.now(timezone.utc)\n",
" if created < now - timedelta(days=TOKEN_TTL_DAYS):\n",
" await users_token_collection.delete_one({'token_hash': token_hash})\n",
" return RedirectResponse(\"/login?\" + urlencode({\"next\": log.get('url', next_url)}))\n",
"\n",
" user_id = log.get(\"user_id\")\n",
" if not user_id:\n",
" return RedirectResponse(\"/login?\" + urlencode({\"next\": log.get('url', next_url)}))\n",
"\n",
" user = await users_collection.find_one({\"_id\": ObjectId(user_id)})\n",
" if not user:\n",
" return RedirectResponse(\"/login?\" + urlencode({\"next\": log.get('url', next_url)}))\n",
"\n",
" user_id_str = str(user[\"_id\"])\n",
" scopes = get_all_user_scopes(user.get(\"roles\", []), user.get(\"extra_scopes\", []))\n",
"\n",
" client_ip = request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" access_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": 'token', \"ip\": client_ip},\n",
" access_exp_delta\n",
" )\n",
" refresh_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": 'token', \"ip\": client_ip},\n",
" refresh_exp_delta\n",
" )\n",
"\n",
" await redis_client.setex(f\"access:{access_token}\", int(access_exp_delta.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{hash_token(refresh_token)}\", int(refresh_exp_delta.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{access_token}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{hash_token(refresh_token)}\")\n",
"\n",
"\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" domain='.kultmost.ru',\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" domain='.kultmost.ru',\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
"\n",
" # Validate and sanitize redirect\n",
" \n",
" if not is_safe_redirect(next_url):\n",
" next_url = '/'\n",
"\n",
" return RedirectResponse(next_url,headers=response.headers)\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "096074ab",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/login\")\n",
"async def login(\n",
" request: Request,\n",
" response: Response,\n",
" login_data: LoginRequest = Body(...),\n",
"):\n",
" identifier = safe_strip(login_data.identifier)\n",
" await check_bruteforce(identifier)\n",
"\n",
" or_conditions = [\n",
" {\"email\": identifier},\n",
" {\"email\": identifier.lower()},\n",
" ]\n",
" \n",
" try:\n",
" normalized_phone = normalize_phone(identifier)\n",
" if normalized_phone:\n",
" or_conditions.append({\"phone\": normalized_phone})\n",
" except:\n",
" pass\n",
" users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n",
"\n",
"\n",
" if not users:\n",
" await increment_failed_attempt(identifier)\n",
" raise HTTPException(401, \"Incorrect credentials\")\n",
"\n",
" # Track if a valid password was found\n",
" valid_user_found = False\n",
"\n",
" for user in users:\n",
" stored = user.get(\"hashed_password\")\n",
"\n",
" if isinstance(stored, bytes):\n",
" stored = stored.decode()\n",
"\n",
" # Check if the password is hashed with Argon2\n",
" if stored.startswith(\"$argon2\"):\n",
" ok = verify_password(login_data.password, stored)\n",
" if ok:\n",
" valid_user_found = True\n",
" break # Password is valid, exit the loop\n",
" \n",
" # Check if the password is hashed with bcrypt\n",
" elif stored.startswith(\"$2y$\"):\n",
" stored = stored.replace(\"$2y$\", \"$2b$\", 1)\n",
" if bcrypt.checkpw(login_data.password.encode(), stored.encode()):\n",
" valid_user_found = True\n",
" new_hash = get_password_hash(login_data.password)\n",
" break # Password is valid, exit the loop\n",
"\n",
" # If no valid user was found, increment attempts and raise error\n",
" if not valid_user_found:\n",
" await increment_failed_attempt(identifier)\n",
" raise HTTPException(401, \"Incorrect credentials\")\n",
"\n",
" # If we reach this point, authentication succeeded\n",
" # Proceed with further logic such as generating tokens or returning user details\n",
"\n",
" # await users_collection.update_one({\"_id\": user[\"_id\"]}, {\"$set\": {\"hashed_password\": new_hash}})\n",
" \n",
" await reset_failed_attempts(identifier)\n",
"\n",
" user_id_str = str(user[\"_id\"])\n",
" scopes = get_all_user_scopes(user.get(\"roles\", []), user.get(\"extra_scopes\", []))\n",
"\n",
" client_ip = request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" access_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": login_data.client_id, \"ip\": client_ip},\n",
" access_exp_delta\n",
" )\n",
" refresh_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": login_data.client_id, \"ip\": client_ip},\n",
" refresh_exp_delta\n",
" )\n",
"\n",
" await redis_client.setex(f\"access:{access_token}\", int(access_exp_delta.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{hash_token(refresh_token)}\", int(refresh_exp_delta.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{access_token}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{hash_token(refresh_token)}\")\n",
"\n",
"\n",
"\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
"\n",
" return {\n",
" \"access_token\": access_token,\n",
" \"refresh_token\": refresh_token,\n",
" \"user_id\": user_id_str,\n",
" \"token_type\": \"bearer\"\n",
" }\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "67f34ef5",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/old/login\")\n",
"async def old_login(\n",
" request: Request,\n",
" response: Response,\n",
"):\n",
" token_old = request.cookies.get('token') or request.cookies.get('v2token')\n",
" access = await get_access_token(request, response)\n",
" if token_old and not access:\n",
" try:\n",
" headers = {'Authorization': f\"Bearer {token_old}\"}\n",
" async with httpx.AsyncClient(http2=True, verify=ctx) as client:\n",
" res = await client.get('https://avtopost.kultmost.ru/api/v1/auth/me', headers=headers)\n",
" email = res.json()['data']['email']\n",
" print(email)\n",
" user = await users_collection.find_one({'email': email})\n",
" except:\n",
" return {'ok': False}\n",
"\n",
" user_id_str = str(user[\"_id\"])\n",
" scopes = get_all_user_scopes(user.get(\"roles\", []), user.get(\"extra_scopes\", []))\n",
"\n",
" client_ip = request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" access_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": 'old_token', \"ip\": client_ip},\n",
" access_exp_delta\n",
" )\n",
" refresh_token = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": 'old_token', \"ip\": client_ip},\n",
" refresh_exp_delta\n",
" )\n",
"\n",
" await redis_client.setex(f\"access:{access_token}\", int(access_exp_delta.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{hash_token(refresh_token)}\", int(refresh_exp_delta.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{access_token}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{hash_token(refresh_token)}\")\n",
"\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=access_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(access_exp_delta.total_seconds())\n",
" )\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=refresh_token,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"Strict\",\n",
" max_age=int(refresh_exp_delta.total_seconds())\n",
" )\n",
" return {'ok': True}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "ff1da72c",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/refresh\", response_model=TokenResponse)\n",
"async def refresh_token(\n",
" request: Request,\n",
" response: Response,\n",
" req: RefreshRequest\n",
"):\n",
" refresh_token = (\n",
" request.cookies.get(\"refresh_token\") or\n",
" get_bearer_token(request) or\n",
" (req.refresh_token if req else None)\n",
" )\n",
"\n",
" if not refresh_token:\n",
" raise HTTPException(401, \"Refresh token required\")\n",
"\n",
" client_id = None\n",
"\n",
" if req:\n",
" client_id = req.client_id\n",
"\n",
" refresh_hash = hash_token(refresh_token)\n",
" user_id_str = await redis_client.get(f\"refresh:{refresh_hash}\")\n",
"\n",
" if not user_id_str:\n",
" raise HTTPException(status_code=401, detail=\"Invalid or expired refresh token\")\n",
"\n",
" try:\n",
" payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])\n",
" if payload.get(\"type\") != \"refresh\":\n",
" raise HTTPException(status_code=401, detail=\"Not a refresh token\")\n",
" if payload.get(\"sub\") != user_id_str:\n",
" raise HTTPException(status_code=401, detail=\"Token mismatch\")\n",
" except jwt.InvalidTokenError as e:\n",
" raise HTTPException(status_code=401, detail=f\"Invalid refresh token: {str(e)}\")\n",
"\n",
" await redis_client.delete(f\"refresh:{refresh_hash}\")\n",
" user = await users_collection.find_one({\"_id\": ObjectId(user_id_str)})\n",
"\n",
" scopes = get_all_user_scopes(user.get(\"roles\", []), user.get(\"extra_scopes\", []))\n",
"\n",
" client_ip = request.headers.get('X-Real-IP') or request.client.host\n",
"\n",
" access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)\n",
" refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
"\n",
" new_access = create_jwt_token(\n",
" {\"sub\": user_id_str, \"scopes\": scopes, \"client_id\": client_id, \"ip\": client_ip},\n",
" access_exp\n",
" )\n",
" new_refresh = create_jwt_token(\n",
" {\"sub\": user_id_str, \"type\": \"refresh\", \"client_id\": client_id, \"ip\": client_ip},\n",
" refresh_exp\n",
" )\n",
"\n",
" new_refresh_hash = hash_token(new_refresh)\n",
" await redis_client.setex(f\"access:{new_access}\", int(access_exp.total_seconds()), user_id_str)\n",
" await redis_client.setex(f\"refresh:{new_refresh_hash}\", int(refresh_exp.total_seconds()), user_id_str)\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" await redis_client.sadd(session_key, f\"access:{new_access}\")\n",
" await redis_client.sadd(session_key, f\"refresh:{new_refresh_hash}\")\n",
"\n",
"\n",
" response.set_cookie(\n",
" key=\"access_token\",\n",
" value=new_access,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"strict\",\n",
" max_age=int(access_exp.total_seconds())\n",
" )\n",
"\n",
" response.set_cookie(\n",
" key=\"refresh_token\",\n",
" value=new_refresh,\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"strict\",\n",
" max_age=int(refresh_exp.total_seconds())\n",
" )\n",
" \n",
"\n",
"\n",
" return {\n",
" \"access_token\": new_access,\n",
" \"token_type\": \"bearer\",\n",
" \"refresh_token\": new_refresh\n",
" }"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "83e1b0f8",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/logout\")\n",
"async def logout(\n",
" request: Request,\n",
" response: Response,\n",
" current_user: Dict = Depends(get_current_user)\n",
"):\n",
" user_id_str = current_user[\"id\"]\n",
"\n",
" session_key = f\"user_sessions:{user_id_str}\"\n",
" tokens = await redis_client.smembers(session_key)\n",
" for t in tokens:\n",
" await redis_client.delete(t)\n",
" await redis_client.delete(session_key)\n",
"\n",
" for cookie_name in request.cookies.keys():\n",
" if cookie_name in ['zoom']:\n",
" continue\n",
"\n",
" for dom in [None,'.kultmost.ru','www.kultmost.ru','avtopost.kultmost.ru','avtopost.kultmost.ru','kultmost.ru']:\n",
" response.delete_cookie(\n",
" key=cookie_name,\n",
" domain=dom,\n",
" path=\"/\", \n",
" secure=False, \n",
" httponly=False,\n",
" )\n",
"\n",
" return {\"message\": \"Logged out from all sessions\"}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5390f2b5",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/api/me\", response_model=dict)\n",
"async def get_me(response: Response, current_user: Dict = Security(get_current_user, scopes=[\"read:self\"])):\n",
" user = display_user(current_user)\n",
" return bson_clear(user)\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "452fab96",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/healthz\")\n",
"async def healthz():\n",
" try:\n",
" await mongo_client.admin.command('ping')\n",
" except Exception as e:\n",
" logging.error(f\"MongoDB is unhealthy: {str(e)}\")\n",
" raise HTTPException(status_code=500, detail=f\"MongoDB is unhealthy: {str(e)}\")\n",
"\n",
" try:\n",
" await redis_client.ping()\n",
" except Exception as e:\n",
" logging.error(f\"Redis is unhealthy: {str(e)}\")\n",
" raise HTTPException(status_code=500, detail=f\"Redis is unhealthy: {str(e)}\")\n",
"\n",
" return {}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "452fab96",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/registration\", response_class=HTMLResponse)\n",
"async def index(\n",
" request: Request,\n",
" next: str | None = None,\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"registration.html\", context={'next': next}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "34f4c829",
"metadata": {},
"outputs": [],
"source": [
"async def get_unique_users(competition_request_col: AsyncIOMotorCollection, competition_id: str, finish: bool = False) -> List[Dict[str, Any]]:\n",
" # Determine match criteria based on finish parameter\n",
" status_match = {\"$match\": {\"posts.status\": \"Проходит\"}} if finish else {\"$match\": {}}\n",
"\n",
" pipeline = [\n",
" {\"$match\": {\"competition_id\": competition_id}},\n",
" {\"$unwind\": \"$user\"},\n",
" {\"$project\": {\n",
" \"user\": 1,\n",
" \"posts\": 1,\n",
" \"status\": 1\n",
" }},\n",
" {\"$unwind\": \"$posts\"},\n",
" status_match, # Use the conditional match here\n",
" {\"$group\": {\n",
" \"_id\": \"$user._id\",\n",
" \"user\": {\"$first\": \"$user\"},\n",
" \"unique_posts\": {\"$addToSet\": \"$posts._id\"},\n",
" \"status\": {\"$first\": \"$status\"}\n",
" }},\n",
" {\"$project\": {\n",
" \"user\": 1,\n",
" \"post_count\": {\"$size\": \"$unique_posts\"},\n",
" \"status\": { \"$cond\": {\"if\": {\"$ne\": [\"$status\", None]}, \"then\": \"$status\", \"else\": False } }\n",
" }}\n",
" ]\n",
" \n",
" cursor = competition_request_col.aggregate(pipeline)\n",
" results = []\n",
" async for doc in cursor:\n",
" results.append(doc)\n",
" \n",
" return results\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "df78c145",
"metadata": {},
"outputs": [],
"source": [
"from typing import Any, Dict, List\n",
"from bson import ObjectId\n",
"from motor.motor_asyncio import AsyncIOMotorCollection\n",
"\n",
"\n",
"async def get_unique_posts_by_user(\n",
" competition_request_col: AsyncIOMotorCollection,\n",
" competition_id: str,\n",
" user_id: str,\n",
") -> list[dict[str, Any]]:\n",
" pipeline = [\n",
" {\"$match\": {\"competition_id\": competition_id, \"user._id\": user_id}},\n",
" {\"$unwind\": \"$posts\"},\n",
" {\n",
" \"$addFields\": {\n",
" \"status_priority\": {\n",
" \"$switch\": {\n",
" \"branches\": [\n",
" {\"case\": {\"$eq\": [\"$posts.status\", \"Проходит\"]}, \"then\": 1},\n",
" {\"case\": {\"$eq\": [\"$posts.status\", \"На рассмотрении\"]}, \"then\": 2},\n",
" ],\n",
" \"default\": 3,\n",
" }\n",
" }\n",
" }\n",
" },\n",
" {\"$sort\": {\"status_priority\": 1, \"posts._id\": 1}}, # tie-breaker if needed\n",
" {\"$group\": {\"_id\": \"$posts._id\", \"post\": {\"$first\": \"$posts\"}}},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$post\"}},\n",
" ]\n",
"\n",
" cursor = competition_request_col.aggregate(pipeline)\n",
" return await cursor.to_list(length=None)"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "8494b389",
"metadata": {},
"outputs": [],
"source": [
"# ignore\n",
"# from typing import Any, Dict, List, Optional\n",
"# from motor.motor_asyncio import AsyncIOMotorCollection\n",
"\n",
"# async def get_unique_posts_by_user(\n",
"# competition_request_col: AsyncIOMotorCollection,\n",
"# competition_id: str,\n",
"# user_id: str,\n",
"# ) -> List[Dict[str, Any]]:\n",
"# docs = await competition_request_col.find({\n",
"# \"competition_id\": competition_id,\n",
"# \"user._id\": user_id,\n",
"# }).to_list(length=None)\n",
"\n",
"# all_posts: List[Dict[str, Any]] = []\n",
"# for d in docs:\n",
"# for p in (d.get(\"posts\") or []):\n",
"# all_posts.append(p)\n",
"\n",
"# def status_priority(status: Optional[str]) -> int:\n",
"# if status == \"Проходит\":\n",
"# return 1\n",
"# if status == \"На рассмотрении\":\n",
"# return 2\n",
"# return 3\n",
"\n",
"# all_posts.sort(\n",
"# key=lambda p: (\n",
"# status_priority(p.get(\"status\")),\n",
"# str(p.get(\"_id\")),\n",
"# )\n",
"# )\n",
"\n",
"# # вместо \"$first\" делаем \"$last\": перезаписываем всегда\n",
"# unique_by_post_id: Dict[Any, Dict[str, Any]] = {}\n",
"# for p in all_posts:\n",
"# pid = p.get(\"_id\")\n",
"# if pid is not None:\n",
"# unique_by_post_id[pid] = p\n",
"\n",
"# posts_ids = list(unique_by_post_id.keys())\n",
"\n",
"# cursor = posts_collection.find({\"_id\": {\"$in\": posts_ids}})\n",
"# return await cursor.to_list(length=None)\n",
"\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "365dc738",
"metadata": {},
"outputs": [],
"source": [
"async def get_unique_posts_с(competition_request_col: AsyncIOMotorCollection, competition_id: str, finish: bool = False) -> List[Any]:\n",
" match_criteria = {\n",
" \"competition_id\": competition_id,\n",
" }\n",
" if finish:\n",
" match_criteria['posts.status'] = 'Проходит'\n",
"\n",
" pipeline = [\n",
" {\"$match\": match_criteria},\n",
" {\"$unwind\": \"$posts\"},\n",
" {\"$match\": {\"posts.status\": \"Проходит\"} if finish else {}},\n",
" {\n",
" \"$group\": {\n",
" \"_id\": \"$posts._id\", # Group by unique post ID\n",
" \"post\": {\"$first\": \"$posts\"} # Retrieve the first occurrence\n",
" }\n",
" },\n",
" {\"$project\": {\"_id\": 0, \"post\": 1}} # Include the post details\n",
" ]\n",
"\n",
" cursor = competition_request_col.aggregate(pipeline)\n",
" unique_posts = await cursor.to_list(length=None) # Fetch all unique posts\n",
" return [unique_post[\"post\"] for unique_post in unique_posts] if unique_posts else []\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6471770b",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/exhibition/{exhibition_id}\", response_class=HTMLResponse)\n",
"async def exhibition_members(\n",
" request: Request,\n",
" exhibition_id: str,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
" if exhibition:\n",
" posts = await delivery_collection.find({'status': 'На выставке', \"exhibition_id\": exhibition_id}).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"exhibition/index.html\", context={\"id\": 1, \"user\": user, \"exhibition\": exhibition, \"posts\": posts}\n",
" )\n",
" else:\n",
" raise HTTPException(status_code=307, headers={\"Location\": '/competitions'}, detail=\"Конкурс не найден\")"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "60ae3fa1",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/competition/{competition_id}\", response_class=HTMLResponse)\n",
"async def competition_members(\n",
" request: Request,\n",
" competition_id: str,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
" if competition:\n",
" members = await get_unique_users(competition_request_col, competition_id, competition['status']==2)\n",
" posts = await get_unique_posts_с(competition_request_col, competition_id, competition['status']==2)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"competition/index.html\", context={\"id\": 1, \"user\": user, \"competition\": competition, \"members\": members, \"posts\": posts}\n",
" )\n",
" else:\n",
" raise HTTPException(status_code=307, headers={\"Location\": '/competitions'}, detail=\"Конкурс не найден\")"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "fddb4c0b",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/competition/{competition_id}/{member_id}\", response_class=HTMLResponse)\n",
"async def competition_member(\n",
" request: Request,\n",
" competition_id: str,\n",
" member_id: str,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
" if competition:\n",
" member = await users_collection.find_one({'_id':ObjectId(member_id)})\n",
" req = await competition_request_col.find_one({'competition_id': competition_id,'user._id': member_id})\n",
" member['status'] = req.get('status', False)\n",
" if member:\n",
" posts = await get_unique_posts_by_user(competition_request_col, competition_id, member_id)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"competition/member/index.html\", context={\"id\": 1, \"user\": user, \"competition\": competition, 'member':member, 'posts': posts}\n",
" )\n",
" else:\n",
" raise HTTPException(status_code=307, headers={\"Location\": f'/competition/{competition._id}'}, detail=\"Участник не найден\")\n",
" else:\n",
" raise HTTPException(status_code=307, headers={\"Location\": '/competitions'}, detail=\"Конкурс не найден\")"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "cf18ebc8",
"metadata": {},
"outputs": [],
"source": [
"async def _serialize(doc):\n",
" doc = doc.copy()\n",
" if \"_id\" in doc:\n",
" doc[\"_id\"] = str(doc[\"_id\"])\n",
" if \"user\" in doc:\n",
" doc[\"user\"] = {'img': doc[\"user\"].get('img'), 'username': doc[\"user\"].get('username')}\n",
" # if \"_id\" in doc[\"user\"]:\n",
" # doc[\"user\"][\"_id\"] = str(doc[\"user\"][\"_id\"])\n",
" # doc[\"user\"][\"notify\"] = []\n",
" return doc\n",
"\n",
"@app.get(\"/api/competitions\", response_model=List)\n",
"async def get_competition():\n",
" competitions = []\n",
" current_time = datetime.now(timezone.utc) + timedelta(hours=3)\n",
"\n",
" async for doc in competitions_col.find({\n",
" 'datetime_start': {'$lt': current_time}\n",
" },{'fb':0,'inst':0,'max':0,'tg':0,'vk':0}).sort('_id',-1).limit(32):\n",
" competitions.append(bson_clear(doc))\n",
" return competitions"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "769746f1",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/exhibitions\", response_class=HTMLResponse)\n",
"async def exhibitions_all(\n",
" request: Request,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" current_time = datetime.now(timezone.utc) + timedelta(hours=3)\n",
" find_list = [\n",
" {'datetime_start': {'$lt': current_time}},\n",
" {'user._id': user.get('id','') if user else ''}\n",
" ]\n",
" if user and 'admin' in user.get('roles',[]):\n",
" find_list.append({'datetime_start': {'$gte': current_time}})\n",
" \n",
" exhibitions = await exhibition_collection.find({\n",
" '$or': find_list,\n",
" 'prod': PROD\n",
" }).sort({'_id':-1}).limit(1000).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"exhibitions.html\", context={\"id\": 1, \"user\": user, \"exhibitions\": exhibitions}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5713a63a",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/competitions\", response_class=HTMLResponse)\n",
"async def competition(\n",
" request: Request,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" competitions = []\n",
" current_time = datetime.now(timezone.utc) + timedelta(hours=3)\n",
" find_list = [\n",
" {'datetime_start': {'$lt': current_time}},\n",
" {'user._id': user.get('id','') if user else ''}\n",
" ]\n",
" if user and 'admin' in user.get('roles',[]):\n",
" find_list.append({'datetime_start': {'$gte': current_time}})\n",
" \n",
" async for doc in competitions_col.find({\n",
" '$or': find_list\n",
" } if PROD else {}).sort({'_id':-1}).limit(1000):\n",
" competitions.append(doc)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"competitions.html\", context={\"id\": 1, \"user\": user, \"competitions\": competitions}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "853822a3",
"metadata": {},
"outputs": [],
"source": [
"router = APIRouter()\n",
"\n",
"\n",
"@router.post(\"/api/restore\")\n",
"async def restore_api(\n",
" request: Request,\n",
" restore_data: RestoreRequest = Body(...),\n",
"):\n",
" # 1. Валидация и очистка входных данных\n",
" identifier = restore_data.identifier.strip()\n",
" \n",
" if not identifier or not is_valid_email(identifier):\n",
" raise HTTPException(status_code=400, detail=\"Некорректный формат email\")\n",
"\n",
" # 2. Проверка анти-спама / ограничения частоты запросов\n",
" client_ip = request.client.host\n",
" rate_limit_key = f\"rate_limit:restore:{identifier}:{client_ip}\"\n",
" \n",
" # Проверяем, запрашивал ли пользователь сброс недавно (например, за последние 60 секунд)\n",
" existing_request = await redis_client.get(rate_limit_key)\n",
" \n",
" if existing_request:\n",
" # Опционально: получаем TTL, чтобы сообщить пользователю, когда можно повторить попытку\n",
" ttl = await redis_client.ttl(rate_limit_key)\n",
" raise HTTPException(\n",
" status_code=429, \n",
" detail=f\"Слишком много запросов. Попробуйте через {ttl} секунд.\"\n",
" )\n",
"\n",
" # Устанавливаем флаг ограничения частоты (истекает через 60 секунд)\n",
" await redis_client.set(rate_limit_key, \"1\", ex=60)\n",
"\n",
" # 3. Поиск пользователя\n",
" user = await users_collection.find_one({'$or': [\n",
" {'email': identifier}, \n",
" {'email': identifier.lower()}\n",
" ]})\n",
" \n",
" if not user:\n",
" # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно\n",
" # Но для удобства использования мы можем сказать \"не найден\".\n",
" # Чтобы предотвратить перечисление, некоторые приложения всегда возвращают \"OK\", но отправляют письмо только если найдено.\n",
" # Здесь мы придерживаемся вашей оригинальной логики, но помните о риске перечисления.\n",
" raise HTTPException(status_code=404, detail=\"Пользователь с таким email не найден\")\n",
" \n",
" # 4. Генерация токена\n",
" token = secrets.token_urlsafe(32)\n",
" expires_at = datetime.now(timezone.utc) + timedelta(hours=1)\n",
" \n",
" token_data = {\n",
" \"user_id\": str(user[\"_id\"]),\n",
" \"email\": user[\"email\"],\n",
" \"next\": restore_data.next,\n",
" \"expires_at\": expires_at.isoformat()\n",
" }\n",
" \n",
" # 5. Сохранение токена сброса в Redis\n",
" reset_token_key = f\"reset_token:{token}\"\n",
" await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600)\n",
"\n",
" # 6. Генерация ссылки для сброса\n",
" base_url = str(request.base_url).rstrip('/')\n",
" reset_link = f\"{base_url}/restore/{token}\"\n",
"\n",
" # 7. Асинхронная отправка email\n",
" try:\n",
" email_task.notify.delay(\n",
" recipient=user['email'],\n",
" context={\n",
" 'link': reset_link, \n",
" 'email': user['email']\n",
" },\n",
" subject='Восстановление пароля',\n",
" template_name='restore.html'\n",
" )\n",
" except Exception as e:\n",
" # Логирование ошибки\n",
" print(f\"Не удалось отправить письмо: {e}\")\n",
" # В зависимости от политики, вы можете захотеть удалить токен, если письмо не отправлено\n",
" await redis_client.delete(reset_token_key)\n",
" raise HTTPException(status_code=500, detail=\"Ошибка отправки письма\")\n",
"\n",
" return {'ok': True}\n",
"\n",
"\n",
"@router.get(\"/restore/{token}\", response_class=HTMLResponse)\n",
"async def restore_page(request: Request, token: str):\n",
" token_key = f\"reset_token:{token}\"\n",
" token_data = await redis_client.get(token_key)\n",
" \n",
" if not token_data:\n",
" return HTMLResponse(\n",
" \"<h2 style='text-align:center;margin-top:100px;'>Ссылка недействительна или истекла</h2>\",\n",
" status_code=400\n",
" )\n",
" \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"restore_token.html\", context={\"token\": token}\n",
" )\n",
"\n",
"\n",
"@router.post(\"/api/reset-password\")\n",
"async def reset_password(\n",
" data: ResetPasswordRequest = Body(...),\n",
"):\n",
" token_key = f\"reset_token:{data.token}\"\n",
" token_data_str = await redis_client.get(token_key)\n",
" \n",
" if not token_data_str:\n",
" raise HTTPException(status_code=400, detail=\"Токен недействителен или истёк\")\n",
" \n",
" token_data = json.loads(token_data_str)\n",
"\n",
" hashed_password = get_password_hash(data.new_password)\n",
" \n",
" await users_collection.update_one(\n",
" {\"_id\": ObjectId(token_data[\"user_id\"])},\n",
" {\"$set\": {\"hashed_password\": hashed_password}}\n",
" )\n",
" \n",
" await redis_client.delete(token_key)\n",
" \n",
" return {\"ok\": True, \"message\": \"Пароль успешно изменён\", \"next\": token_data.get('next', '/profile')}\n",
"\n",
"\n",
"@router.get(\"/restore\", response_class=HTMLResponse)\n",
"async def restore_init(\n",
" request: Request,\n",
" next: str | None = None,\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"restore.html\", context={'next': next}\n",
" )\n",
"\n",
"\n",
"app.include_router(router, tags=['Восстановление пароля'])"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "eafaa621",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/login\", response_class=HTMLResponse)\n",
"async def login_(\n",
" request: Request,\n",
" next_url: str | None = None,\n",
"):\n",
" if not next_url:\n",
" next_url = request.headers.get(\"Referer\")\n",
" context={\"next\": next_url}\n",
" print(next_url)\n",
" if next_url and 'profile/competition' in next_url:\n",
" context.update({'notify':{'text':'Участвовать в конкурсе могут только пользователи сервиса! Пожалуйста, войдите или зарегистрируйтесь.', 'status':'info','duration':8000}})\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"login.html\",\n",
" context=context,\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "b61deb40",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/switch-profile\")\n",
"async def switch_profile(request: Request,\n",
" response: Response,user: Dict = Security(get_current_user)):\n",
" await users_collection.update_one({'_id':ObjectId(user.get('_id'))},{'$set':{'profile':'' if user.get('profile',False) else 'partner'}})\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "b22ace38",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/post/description\")\n",
"async def post_edit_description(\n",
" data: PostEditDescription = Body(...),\n",
" current_user: Dict = Security(get_current_user)\n",
"):\n",
" await posts_collection.update_one(\n",
" {'_id':ObjectId(data.post_id), 'user._id': current_user.get('_id')},\n",
" {'$set': {'description': data.description}}\n",
" )\n",
" return {'ok': True}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "dbd15e8b",
"metadata": {},
"outputs": [],
"source": [
"@app.delete(\"/api/partner/exhibition/delete\")\n",
"async def partner_exhibition_delete(\n",
" data: PartnerExhibitionDelete = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" exhibition = await exhibition_collection.find_one_and_delete(\n",
" {'_id':ObjectId(data.exhibition_id), 'user._id': current_user.get('_id')},\n",
" )\n",
" for i in exhibition.get('tg',[]):\n",
" tg.delete_from_tg.delay(i.get('result',[{}])[0].get('message_id'), i.get('doc'))\n",
" for i in exhibition.get('vk',[]):\n",
" vk.delete_wall.delay(i.get('response',{}).get('post_id'), i.get('doc'))\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5c514008",
"metadata": {},
"outputs": [],
"source": [
"@app.delete(\"/api/partner/competition/delete\")\n",
"async def partner_competition_delete(\n",
" data: Partnercompetitionstop = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" competition = await competitions_col.delete_one(\n",
" {'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')},\n",
" )\n",
" for i in competition.get('tg',[]):\n",
" tg.delete_from_tg.delay(i.get('result',[{}])[0].get('message_id'), i.get('doc'))\n",
" for i in competition.get('vk',[]):\n",
" vk.delete_wall.delay(i.get('response',{}).get('post_id'), i.get('doc'))\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "0a6e98ee",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/partner/competition/finish\")\n",
"async def partner_competition_finish(\n",
" data: Partnercompetitionstop = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" await competitions_col.update_one(\n",
" {'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')},\n",
" {'$set': {'status': 2}},\n",
" )\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "e5d0e790",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/partner/competition/stop\")\n",
"async def partner_competition_stop(\n",
" data: Partnercompetitionstop = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" await competitions_col.update_one(\n",
" {'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')},\n",
" {'$set': {'status': 1}},\n",
" )\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "9397d524",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/partner/competition/select\")\n",
"async def partner_competition_request_artist(\n",
" background_tasks: BackgroundTasks,\n",
" data: CompetitionArtistRequest = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" print(data)\n",
" competition = await competitions_col.find_one({'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')})\n",
" if competition:\n",
" await competition_request_col.update_many(\n",
" {'user._id': data.user_id, 'competition_id': data.competition_id},\n",
" {'$set': {'status': True,\"posts.$[].status\": 'Проходит' }},\n",
" )\n",
" background_tasks.add_task(accept_competition_req, competition, data.user_id)\n",
" social_vk = await social_collection.find_one({'user_id':current_user.get('_id'), 'group': {\"$exists\": False}, 'type':'vk'})\n",
" user = display_user(current_user)\n",
" user['social_vk'] = social_vk\n",
" await invitations_collection.update_one({\n",
" \"user_id\": data.user_id,\n",
" \"item\": competition.get('title'),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": user,\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" },{'$set':{\n",
" \"user_id\": data.user_id,\n",
" \"item\": competition.get('title'),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": user,\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" }},upsert=True)\n",
" return {'ok': True}\n",
" else:\n",
" return {'ok': False}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "032576ee",
"metadata": {},
"outputs": [],
"source": [
"from fastapi import HTTPException, BackgroundTasks, Body, Security\n",
"from bson import ObjectId\n",
"from typing import Dict\n",
"\n",
"@app.post(\n",
" \"/api/partner/competition/request\",\n",
" response_model=dict,\n",
" summary=\"Обновление статуса заявки участника в конкурсе (для партнёра)\",\n",
" description=\"\"\"\n",
" Позволяет владельцу конкурса обновить статус конкретного поста в заявке.\n",
" \n",
" - Проверяет права владельца конкурса.\n",
" - Обновляет статус поста в массиве `posts`.\n",
" - Если после обновления все посты в заявке имеют статус \"Проходит\" или \"На рассмотрении\" — \n",
" заявка помечается `notify: True` и запускается фоновая обработка.\n",
" \"\"\",\n",
" status_code=200,\n",
" tags=[\"Partner\", \"Competition\", \"Requests\"],\n",
")\n",
"async def partner_competition_request(\n",
" background_tasks: BackgroundTasks,\n",
" data: PartnercompetitionRequest = Body(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" \"\"\"\n",
" Обработчик обновления статуса поста в заявке на конкурс.\n",
"\n",
" Flow:\n",
" 1. Проверка владения конкурсом.\n",
" 2. Массовое обновление статуса поста в массиве заявок.\n",
" 3. Проверка полностью одобренных заявок → установка `notify` + фоновая задача.\n",
" \"\"\"\n",
" # Проверка прав владельца конкурса\n",
" competition = await competitions_col.find_one({\n",
" '_id': ObjectId(data.competition_id),\n",
" 'user._id': current_user.get('_id')\n",
" })\n",
"\n",
" if not competition:\n",
" raise HTTPException(\n",
" status_code=403,\n",
" detail=\"Конкурс не найден или у вас нет прав на его редактирование\"\n",
" )\n",
"\n",
" # Обновляем статус конкретного поста\n",
" await competition_request_col.update_many(\n",
" {\n",
" 'posts._id': ObjectId(data.post_id),\n",
" 'competition_id': data.competition_id\n",
" },\n",
" {'$set': {'posts.$[p].status': data.status}},\n",
" array_filters=[{'p._id': ObjectId(data.post_id)}]\n",
" )\n",
"\n",
" posts_list = []\n",
" user = None\n",
"\n",
" async for req in competition_request_col.find({\n",
" 'posts._id': ObjectId(data.post_id),\n",
" 'competition_id': data.competition_id,\n",
" }):\n",
" if not user:\n",
" user = req.get('user')\n",
" \n",
" posts_list.extend(req.get('posts', []))\n",
"\n",
" unique_posts = {str(p.get('_id')): p for p in posts_list}.values()\n",
" \n",
" background_tasks.add_task(update_competition_req, competition, user, unique_posts)\n",
"\n",
"\n",
" return {'ok': True}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "25fec0c7",
"metadata": {},
"outputs": [],
"source": [
"async def _update_post_description(post):\n",
" if post.get(\"description\"):\n",
" await posts_collection.update_one(\n",
" {\"_id\": ObjectId(post[\"postId\"])},\n",
" {\"$set\": {\"description\": post[\"description\"]}}\n",
" )\n",
"\n",
"\n",
"async def background_request_competition(posts: List[dict]):\n",
" coros = [_update_post_description(p) for p in posts]\n",
" if coros:\n",
" await asyncio.gather(*coros)"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "b44b4b2e",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/send_competition_request/{competition_id}\")\n",
"async def send_competition_request(\n",
" request: Request,\n",
" competition_id: str,\n",
" background_tasks: BackgroundTasks,\n",
" user: Dict = Security(get_current_user),\n",
"):\n",
" try:\n",
" data = await request.json()\n",
" except Exception:\n",
" raise HTTPException(status_code=400, detail=\"Invalid JSON\")\n",
"\n",
" posts_input = data.get(\"posts\") or []\n",
" if not posts_input:\n",
" raise HTTPException(status_code=400, detail=\"Поле 'posts' обязательно\")\n",
"\n",
" # Build mapping: postId -> nomination (if provided)\n",
" nomination_map: dict[str, str | None] = {}\n",
" post_ids: list[str] = []\n",
"\n",
" for p in posts_input:\n",
" post_id = p.get(\"postId\")\n",
" if post_id:\n",
" post_ids.append(post_id)\n",
" nomination_map[post_id] = p.get(\"nomination\") # can be str or None\n",
"\n",
" unique_post_ids = list(dict.fromkeys(post_ids))\n",
"\n",
" if not unique_post_ids:\n",
" raise HTTPException(status_code=400, detail=\"Нет валидных postId\")\n",
"\n",
" # Validate ObjectIds\n",
" try:\n",
" obj_ids = [ObjectId(pid) for pid in unique_post_ids]\n",
" except Exception:\n",
" raise HTTPException(status_code=400, detail=\"Некорректный формат postId\")\n",
"\n",
" # Check already used posts\n",
" used_post_ids: set[str] = set()\n",
" async for req in competition_request_col.find({\n",
" \"user._id\": user[\"_id\"],\n",
" \"competition_id\": competition_id\n",
" }):\n",
" for post in req.get(\"posts\", []):\n",
" if post_id := post.get(\"_id\"):\n",
" used_post_ids.add(str(post_id))\n",
"\n",
" new_obj_ids = [oid for oid in obj_ids if str(oid) not in used_post_ids]\n",
" excluded_count = len(obj_ids) - len(new_obj_ids)\n",
"\n",
" if not new_obj_ids:\n",
" return {\"ok\": True, \"message\": \"Все посты уже были отправлены ранее\"}\n",
"\n",
" # Fetch full posts\n",
" cursor = posts_collection.find({\"_id\": {\"$in\": new_obj_ids}})\n",
" fetched_posts = [doc async for doc in cursor]\n",
"\n",
" # Enrich posts with nomination from input\n",
" for post in fetched_posts:\n",
" post_id_str = str(post.get(\"_id\"))\n",
" if nomination := nomination_map.get(post_id_str):\n",
" post[\"nomination\"] = nomination\n",
"\n",
" doc = {\n",
" \"user\": display_user(user),\n",
" \"competition_id\": competition_id,\n",
" \"posts\": fetched_posts,\n",
" \"created_at\": datetime.utcnow(),\n",
" }\n",
"\n",
" result = await competition_request_col.insert_one(doc)\n",
"\n",
" # Background tasks\n",
" background_tasks.add_task(background_request_competition, posts_input) # pass original input\n",
" background_tasks.add_task(new_competiton_req, bson_clear(doc))\n",
"\n",
" response = {\n",
" \"ok\": True,\n",
" \"inserted_id\": str(result.inserted_id)\n",
" }\n",
"\n",
" if excluded_count > 0:\n",
" response[\"excluded\"] = excluded_count\n",
" response[\"message\"] = f\"Исключено {excluded_count} уже использованных постов\"\n",
"\n",
" return response"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "11de4903",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/news\", response_class=HTMLResponse)\n",
"async def profile_news(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" news = [doc async for doc in news_collection.find({'user._id': user.get('_id')}).sort('_id',-1).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/news.html\", context={ \"user\": user, 'news': news}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "7cda79dd",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/form\", response_class=HTMLResponse)\n",
"async def profile_form(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/form.html\", context={ \"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "2827fcab",
"metadata": {},
"outputs": [],
"source": [
"async def competitions(user,request):\n",
" data=[]\n",
" async for i in competitions_col.find({'user._id': user.get('_id')}).sort('_id',-1).limit(1000):\n",
" data.append(i)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/competitions.html\", context={ \"user\": user, 'cards': data}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "220f51ed",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/partner/competitions\", response_class=HTMLResponse)\n",
"async def profile_partner_competitions(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" await users_collection.update_one({'_id':ObjectId(user.get('_id'))},{'$set':{'profile':'partner'}})\n",
" user['profile'] = 'partner'\n",
" return await competitions(user,request)"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "afabbc56",
"metadata": {},
"outputs": [],
"source": [
"async def get_unique_posts(competition_request_col: AsyncIOMotorCollection, competition_id: str) -> List[Dict[str, Any]]:\n",
" pipeline = [\n",
" {\"$match\": {\"competition_id\": competition_id}},\n",
" {\"$project\": {\"posts\": 1, \"_id\": 0}},\n",
" {\"$unwind\": \"$posts\"},\n",
" {\"$group\": {\n",
" \"_id\": \"$posts._id\", # group by post _id for uniqueness\n",
" \"post\": {\"$first\": \"$posts\"}\n",
" }},\n",
" {\"$addFields\": {\n",
" \"status_order\": {\n",
" \"$switch\": {\n",
" \"branches\": [\n",
" {\"case\": {\"$eq\": [\"$post.status\", 'Проходит']}, \"then\": 2},\n",
" {\"case\": {\"$eq\": [\"$post.status\", 'На рассмотрении']}, \"then\": 3}\n",
" ],\n",
" \"default\": 1\n",
" }\n",
" }\n",
" }},\n",
" {\"$sort\": {\"status_order\": 1}},\n",
" {\"$project\": {\"post\": 1, \"_id\": 0}} \n",
" ]\n",
" \n",
" cursor = competition_request_col.aggregate(pipeline)\n",
" results = []\n",
" \n",
" async for doc in cursor:\n",
" results.append(doc['post']) # Adjust based on the added $project stage\n",
" return results\n",
"\n",
"\n",
"async def add_competition_id_to_posts(posts, competition_id):\n",
" oid = ObjectId(competition_id)\n",
" for post in posts:\n",
" post['competition'] = oid\n",
" return posts\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6ce1690c",
"metadata": {},
"outputs": [],
"source": [
"async def get_unique_users_p(competition_request_col: AsyncIOMotorCollection, competition_id: str) -> List[Dict[str, Any]]:\n",
" pipeline = [\n",
" {\"$match\": {\"competition_id\": competition_id}},\n",
" {\"$unwind\": \"$user\"},\n",
" {\"$project\": {\n",
" \"user\": 1,\n",
" \"posts\": 1,\n",
" \"status\": 1\n",
" }},\n",
" {\"$unwind\": \"$posts\"},\n",
" {\"$group\": {\n",
" \"_id\": \"$user._id\",\n",
" \"user\": {\"$first\": \"$user\"},\n",
" \"unique_posts\": {\"$addToSet\": \"$posts._id\"},\n",
" \"status\": {\"$first\": \"$status\"}\n",
" }},\n",
" {\"$project\": {\n",
" \"user\": 1,\n",
" \"post_count\": {\"$size\": \"$unique_posts\"},\n",
" \"status\": { \"$cond\": { \"if\": {\"$ne\": [\"$status\", None]}, \"then\": \"$status\", \"else\": False}}\n",
" }},\n",
" {\"$sort\": {\"status\": 1, \"_id\": 1}}\n",
" ]\n",
" \n",
" cursor = competition_request_col.aggregate(pipeline)\n",
" results = []\n",
" async for doc in cursor:\n",
" results.append(doc)\n",
" \n",
" return results\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5e4a0d74",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/partner/competition/{competition_id}\", response_class=HTMLResponse)\n",
"async def profile_partner_competition(\n",
" request: Request,\n",
" competition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id), 'user._id': user.get('_id')})\n",
" if not competition:\n",
" raise HTTPException(status_code=403, detail=\"Not enough permissions\")\n",
" \n",
" members = await get_unique_users_p(competition_request_col, competition_id)\n",
"\n",
" posts = await get_unique_posts(competition_request_col, competition_id)\n",
" posts = await add_competition_id_to_posts(posts, competition_id) \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/competition.html\", context={ \"user\": user, \"competition\": competition, 'posts': posts, \"members\": members}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba9996c8",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/partner/competition/{competition_id}/{member_id}\", response_class=HTMLResponse)\n",
"async def profile_partner_competition_member(\n",
" request: Request,\n",
" competition_id: str,\n",
" member_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id), 'user._id': user.get('_id')})\n",
" if not competition:\n",
" raise HTTPException(status_code=403, detail=\"Not enough permissions\")\n",
" \n",
" social_vk = await social_collection.find_one({'user_id':member_id,'type':'vk','group':{\"$exists\": False}})\n",
" \n",
" member = await users_collection.find_one({'_id':ObjectId(member_id)})\n",
" req = await competition_request_col.find_one({'competition_id': competition_id,'user._id': member_id})\n",
" member['status'] = req.get('status', False)\n",
" posts = await get_unique_posts_by_user(competition_request_col, competition_id, member_id)\n",
" posts = await add_competition_id_to_posts(posts, competition_id) \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/competition_member.html\", context={ \"user\": user, \"social_vk\":social_vk, \"competition\": competition, 'posts': posts, \"member\": member}\n",
" )\n",
" "
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5500761",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/competition\", response_class=HTMLResponse)\n",
"async def profile_competitions(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" competitions = []\n",
" current_time = datetime.now(timezone.utc) + timedelta(hours=3)\n",
"\n",
" async for i in competitions_col.find({\n",
" '$or': [\n",
" {'datetime_start': {'$lt': current_time}},\n",
" {'user._id': user.get('_id')}\n",
" ]\n",
" } if PROD else {}).sort('_id', -1).limit(1000):\n",
" competitions.append(i)\n",
" \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/competition.html\", context={ \"user\": user, 'competitions': competitions}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "c01c6aa7",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/competition/{competition_id}\", response_class=HTMLResponse)\n",
"async def profile_competition(\n",
" request: Request,\n",
" competition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" # required_keys = [\n",
" # \"biography\",\n",
" # \"messengers\",\n",
" # \"style\",\n",
" # \"material_manufacturer\",\n",
" # \"genres\",\n",
" # \"education\", \"material_manufacturer\",\n",
" # \"art_types\", \"material_use\",\n",
" # \"unions\", \"master_classes\",\n",
" # \"style\", \"workshop_tour\",\n",
" # \"specialization\",\n",
" # ]\n",
"\n",
" # if all(user.get(key) for key in required_keys):\n",
" competition = await competitions_col.find_one({'_id':ObjectId(competition_id)})\n",
" posts=[]\n",
" async for i in posts_collection.find({'user._id': user.get('_id')}).sort('_id',-1).limit(1000):\n",
" posts.append(i)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/competition/index.html\", context={\n",
" \"user\": user, 'competition': competition, 'posts': posts,\n",
" 'notify':{'text':'Выберите картины на конкурс','status':'info','duration':3000}\n",
" }\n",
" )\n",
" # else:\n",
" # return templates.TemplateResponse(\n",
" # request=request, name=\"profile/form.html\", context={ \"user\": user, 'notify':{'text':'В начале заполните анкету под звездочками','status':'error','duration':8000}}\n",
" # )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "3eb9e736",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile\", response_class=HTMLResponse)\n",
"async def profile(\n",
" request: Request,\n",
" response: Response,\n",
" user: Dict = Security(get_current_user)\n",
"): \n",
" if user.get('profile',False):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/index.html\", context={ \"user\": user, 'notify': {'text': 'Кабинет партнера', 'status': 'info', 'duration': 1500}}\n",
" )\n",
" else:\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/index.html\", context={ \"user\": user}\n",
" )\n",
"\n",
"\n",
"@app.get(\"/profile/partner\", response_class=HTMLResponse)\n",
"async def profile(\n",
" request: Request,\n",
" response: Response,\n",
" user: Dict = Security(get_current_user)\n",
"): \n",
" if user.get('profile',False):\n",
" return await competitions(user,request)\n",
" else:\n",
" return await posts_temp(user,request)\n",
"\n",
"\n",
"@app.get(\"/profile/posts\", response_class=HTMLResponse)\n",
"async def profile_posts(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" return await posts_temp(user,request)\n",
"\n",
"\n",
"@app.get(\"/profile/password\", response_class=HTMLResponse)\n",
"async def profile_password(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/password.html\", context={\"user\": user}\n",
" )\n",
"\n",
"\n",
"@app.get(\"/profile/exhibition\", response_class=HTMLResponse)\n",
"async def profile_exhibition(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" # data = exhibition_collection.\n",
" data = await exhibition_collection.find({}).sort('_id', -1).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/exhibition.html\", context={\"user\": user, \"data\": data, \"now\": datetime.now()}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "95b2823a",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/\")\n",
"async def index(request: Request, user: Dict = Security(get_user)):\n",
" news = await get_latest_news()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"index.html\", context={\"user\": user, \"news\": news}\n",
" )\n",
"\n",
"\n",
"@app.get(\"/about\")\n",
"async def about(request: Request, user: Dict = Security(get_user)):\n",
" news = await get_latest_news()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"about.html\", context={\"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "14ebeb80",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/news\")\n",
"async def news(request: Request, user: Dict = Security(get_user)):\n",
" news = [doc async for doc in news_collection.find({}).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"news.html\", context={\"user\": user, \"news\": news}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6478802f",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/pwa\")\n",
"async def pwa(request: Request, user: Dict = Security(get_user)):\n",
" news = await get_latest_news()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"index.html\", context={\"user\": user, \"news\": news, 'pwa':True}\n",
" )\n",
"\n",
"@app.get(\"/pwa2\")\n",
"async def pwa(request: Request, user: Dict = Security(get_user)):\n",
" news = await get_latest_news()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"index.html\", context={\"user\": user, \"news\": news, 'pwa':True}\n",
" )\n",
"\n",
"@app.get(\"/offline.html\")\n",
"async def offline_html(request: Request, user: Dict = Security(get_user)):\n",
" news = await get_latest_news()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"index.html\", context={\"user\": user, \"news\": news, 'pwa':True}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "642439cc",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/partner/galerist\", response_class=HTMLResponse)\n",
"async def partner_galerist(\n",
" request: Request,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" partner = [doc async for doc in users_collection.find({'roles': 'partner'},{'username': 1, 'description': 1})]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"partner/galerist.html\", context={\"id\": 1, \"user\": user, \"partner\": partner}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e2432c8",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/partner\", response_class=HTMLResponse)\n",
"async def partner(\n",
" request: Request,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"partner/index.html\", context={\"id\": 1, \"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d1880762",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/artists\", response_class=HTMLResponse)\n",
"async def artists(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"users:read\"])\n",
"):\n",
" users = [doc async for doc in users_collection.find({'roles':'user'}).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"users/index.html\", context={\"id\": 1, \"user\":user, \"users\": users}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "83105c5e",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/my-paints\", response_class=HTMLResponse)\n",
"async def My_paints(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"read:self\"])\n",
"):\n",
" user_id = user.get('_id')\n",
" if isinstance(user_id, int) or (is_str:=len(user_id) < 5):\n",
" doc = await users_collection.find_one({\"id\": int(user_id) if is_str else user_id}, {\"_id\": 1})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Пользователь не найден\")\n",
" return RedirectResponse(url=f\"/artists/{doc.get('_id')}\", status_code=301)\n",
"\n",
" doc = users_collection.find_one({\"_id\": user_id})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Пользователь не найден\")\n",
"\n",
" total_key = f\"total_w_c_artists:{user_id}\"\n",
" today_key = f\"today_w_c_artists:{user_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
"\n",
" find = await users_collection.find_one({'_id':ObjectId(user_id)})\n",
" cards = [i async for i in posts_collection.find({'user._id': user_id}).sort('_id',-1).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"users/user.html\", context={\"id\": 1, \"user\":user, \"find\": find, \"cards\": cards,\"total_watch_count\": total_watch_count,\"today_watch_count\": today_watch_count,}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "ff099b10",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/artists/{user_id}\", response_class=HTMLResponse)\n",
"async def artists_id(\n",
" request: Request,\n",
" user_id: Union[int, str],\n",
" user: Dict = Security(get_current_user, scopes=[\"users:read\"])\n",
"):\n",
" if isinstance(user_id, int) or (is_str:=len(user_id) < 5):\n",
" doc = await users_collection.find_one({\"id\": int(user_id) if is_str else user_id}, {\"_id\": 1})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Пользователь не найден\")\n",
" return RedirectResponse(url=f\"/artists/{doc.get('_id')}\", status_code=301)\n",
"\n",
" doc = users_collection.find_one({\"_id\": user_id})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Пользователь не найден\")\n",
"\n",
" total_key = f\"total_w_c_artists:{user_id}\"\n",
" today_key = f\"today_w_c_artists:{user_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
"\n",
" find = await users_collection.find_one({'_id':ObjectId(user_id)})\n",
" cards = [i async for i in posts_collection.find({'user._id': user_id}).sort('_id',-1).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"users/user.html\", context={\"id\": 1, \"user\":user, \"find\": find, \"cards\": cards,\"total_watch_count\": total_watch_count,\"today_watch_count\": today_watch_count,}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "e3ad237d",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/competition\", response_class=HTMLResponse)\n",
"async def form_(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"): \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/competition.html\", context={\"id\": 1, \"user\": user}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "fae437a1",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/competition/{competition_id}\", response_class=HTMLResponse)\n",
"async def form_(\n",
" request: Request,\n",
" competition_id: str,\n",
" user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"): \n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
" data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/competition_edit.html\", context={\"competition\": str(data), \"competition_id\": competition_id, \"user\": user}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d1375e3",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/news/{news_id}')\n",
"async def article_page(\n",
" news_id: Union[int, str],\n",
" requset: Request,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" if isinstance(news_id, int) or (is_str:=len(news_id) < 5):\n",
" doc = await news_collection.find_one({\"id\": int(news_id) if is_str else news_id}, {\"_id\": 1})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"новость не найдена\")\n",
" return RedirectResponse(url=f\"/news/{doc.get('_id')}\", status_code=301)\n",
" news = await news_collection.find_one({'_id':ObjectId(news_id)})\n",
" if not news:\n",
" raise HTTPException(status_code=404, detail=\"Новость не найдена\")\n",
" return templates.TemplateResponse(\n",
" request=requset, name='news/index.html', context={\"user\":user, \"news\": news}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "a330f47f",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/post\", response_class=HTMLResponse)\n",
"async def form_post(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" partner = [doc async for doc in users_collection.find({'roles': 'partner'},{'username': 1})]\n",
" service_social = [doc async for doc in social_collection.find({'service': True})]\n",
" user_social = [doc async for doc in social_collection.find({'active': True,'service': {'$exists': False},'user_id': str(user.get('_id'))})]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"posts/form.html\", context={\"id\": 1, \"user\": user, 'user_social': user_social, 'partner': partner, 'service_social': service_social}\n",
" )\n",
"\n",
"@app.get(\"/form/post/{post_id}\", response_class=HTMLResponse)\n",
"async def form_post_edit(\n",
" request: Request,\n",
" post_id:str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" post = await posts_collection.find_one({'_id':ObjectId(post_id)})\n",
" data = json.dumps(post, cls=MongoJSONEncoder, ensure_ascii=False)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/post_edit.html\", context={\"post\": data, \"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "136946a5",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/news\", response_class=HTMLResponse)\n",
"async def form_news(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/news.html\", context={\"id\": 1, \"user\": user}\n",
" )\n",
"\n",
"@app.get(\"/form/news/{news_id}\", response_class=HTMLResponse)\n",
"async def form_news_edit(\n",
" request: Request,\n",
" news_id:str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" news = await news_collection.find_one({'_id':ObjectId(news_id)})\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/news_edit.html\", context={\"news\": news, \"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "f6c5b785",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/exhibition\", response_class=HTMLResponse)\n",
"async def form_exhibition(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/exhibition.html\", context={\"id\": 1, \"user\": user}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d6c3a62d",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/form/exhibition/{exhibition_id}\", response_class=HTMLResponse)\n",
"async def form_exhibition(\n",
" request: Request,\n",
" exhibition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)},{'posts':0,'vk':0,'tg':0,'fb':0,'inst':0,'max':0})\n",
" data = json.dumps(exhibition, cls=MongoJSONEncoder, ensure_ascii=False)\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"form/exhibition_edit.html\", context={\"id\": 1, \"user\": user, \"exhibition\": str(data)}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5f2da076",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/avatar\", response_model=dict)\n",
"async def avatar_change(\n",
" background_tasks: BackgroundTasks,\n",
" avatar: Optional[UploadFile] = File(...),\n",
" current_user: Dict = Security(get_current_user)\n",
"):\n",
" try:\n",
" avatar_id = await save_to_gridfs(avatar, 'users')\n",
" await users_collection.update_one({'_id': ObjectId(current_user.get('_id'))}, {'$set': {'img': avatar_id}})\n",
" current_user.update({'img': avatar_id})\n",
" background_tasks.add_task(logo_user_task, bson_clear(current_user))\n",
" return {'ok': True}\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "f952a708",
"metadata": {},
"outputs": [],
"source": [
"@app.put(\"/api/user/form\", response_model=dict)\n",
"async def user_form(\n",
" request: Request,\n",
" current_user: Dict = Security(get_current_user)\n",
"):\n",
" try:\n",
" doc = await request.json()\n",
" if not isinstance(doc, dict):\n",
" raise HTTPException(status_code=400, detail=\"Invalid JSON body\")\n",
"\n",
" # Remove any protected fields from incoming update\n",
" update_doc = {k: v for k, v in doc.items() if k not in PROTECTED_FIELDS_USER}\n",
"\n",
" if not update_doc:\n",
" return {\"ok\": True, \"updated\": False, \"reason\": \"no allowed fields\"}\n",
"\n",
" result = users_collection.update_one(\n",
" {\"_id\": ObjectId(current_user[\"id\"])},\n",
" {\"$set\": update_doc}\n",
" )\n",
"\n",
" return {\"ok\": True, \"updated\": True}\n",
" except HTTPException:\n",
" raise\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6971b35c",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/paintings/{paint_id}')\n",
"async def paintings(\n",
" request: Request,\n",
" paint_id: Union[int, str],\n",
" user: Dict = Security(get_user)\n",
"):\n",
" if isinstance(paint_id, int) or (is_str:=len(paint_id) < 5):\n",
" doc = await posts_collection.find_one({\"id\": int(paint_id) if is_str else paint_id}, {\"_id\": 1})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Картина не найдена\")\n",
" return RedirectResponse(url=f\"/paintings/{doc.get('_id')}\", status_code=301)\n",
"\n",
" paint = await posts_collection.find_one({'_id': ObjectId(paint_id)})\n",
" if not paint:\n",
" raise HTTPException(status_code=404, detail=\"Картина не найдена\")\n",
" total_key = f\"total_watch_count_paint:{paint_id}\"\n",
" today_key = f\"today_watch_count_paint:{paint_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
" \n",
" description = f\"{paint['author']}\\n\"\n",
" description += f\"Размер: {paint['size']}\\n\"\n",
" description += f\"Материал: {paint['material']}\\n\"\n",
" description += f\"Стоимость: {paint['price']}\\n\"\n",
" description += f\"{paint['description']}\"\n",
"\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"paintings/paint.html\", context={\"paint\": paint, \"user\": user, \"description\": description,'today_watch_count':today_watch_count,'total_watch_count':total_watch_count}\n",
" )\n",
"\n",
"\n",
"@app.get('/post/{paint_id}')\n",
"async def old_post(\n",
" request: Request,\n",
" paint_id: int,\n",
" user: Dict = Security(get_user)\n",
"):\n",
" if isinstance(paint_id, int):\n",
" doc = await posts_collection.find_one({\"id\": paint_id}, {\"_id\": 1})\n",
" if not doc:\n",
" raise HTTPException(status_code=404, detail=\"Картина не найдена\")\n",
" return RedirectResponse(url=f\"/paintings/{doc.get('_id')}\", status_code=301)"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "aabb40cb",
"metadata": {},
"outputs": [],
"source": [
"@app.delete(\"/api/post/{post_id}\", response_model=dict)\n",
"async def post_delete(\n",
" post_id: Optional[str],\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" await posts_collection.delete_one({'_id': ObjectId(post_id), 'user._id': current_user.get('_id')})\n",
" return {'ok': True} \n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d38cafed",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/post\", response_model=dict)\n",
"async def post_painting(\n",
" background_tasks: BackgroundTasks,\n",
" author: str = Form(None),\n",
" grade: str = Form(None),\n",
" city: str = Form(None),\n",
" title: str = Form(...),\n",
" size: str = Form(None),\n",
" material: str = Form(None),\n",
" genre: str = Form(None),\n",
" price: str = Form(None),\n",
" description: str = Form(None),\n",
" post_medias: Optional[List[str]] = Form(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" try:\n",
" doc = {\n",
" \"title\": safe_strip(title),\n",
" \"url\": transliterate(f\"{safe_strip(title)} {safe_strip(author, current_user['username'])} {safe_strip(city)}\"),\n",
" \"author\": safe_strip(author, current_user['username']),\n",
" \"grade\": safe_strip(grade),\n",
" \"city\": safe_strip(city) or current_user.get('living_place'),\n",
" \"size\": safe_strip(size),\n",
" \"status\": \"В каталоге\",\n",
" \"material\": safe_strip(material),\n",
" \"genre\": safe_strip(genre),\n",
" \"price\": safe_strip(price),\n",
" \"description\": safe_strip(description),\n",
" \"medias\": post_medias or [],\n",
" \"medias_original\": post_medias or [],\n",
" \"user\": display_user(current_user),\n",
" **{i:[] for i in SOCIALS},\n",
" }\n",
" \n",
" res = await posts_collection.insert_one(doc)\n",
" doc_out = {\n",
" \"id\": str(res.inserted_id),\n",
" \"_id\": str(res.inserted_id),\n",
" **{k: v for k, v in doc.items()}\n",
" }\n",
" doc_out = bson_clear(doc_out)\n",
" background_tasks.add_task(replace_post_medias, doc_out)\n",
" if PROD:\n",
" background_tasks.add_task(email_new_post, doc_out)\n",
" background_tasks.add_task(publish_social_post, doc_out)\n",
" if 'user' in current_user.get('roles',[]) and safe_strip(grade) != current_user.get('grade'):\n",
" await users_collection.update_one({'_id':ObjectId(current_user.get('_id'))},{\"$set\": {'grade': safe_strip(grade)}})\n",
" return doc_out\n",
" except Exception as e:\n",
" print(e)\n",
" raise HTTPException(status_code=500, detail=str(e))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "44ea690c",
"metadata": {},
"outputs": [],
"source": [
"@app.put(\"/api/post/{post_id}\", response_model=dict)\n",
"async def put_painting(\n",
" background_tasks: BackgroundTasks,\n",
" post_id: str,\n",
" author: str = Form(None),\n",
" grade: str = Form(None),\n",
" city: str = Form(None),\n",
" title: str = Form(...),\n",
" size: str = Form(None),\n",
" material: str = Form(None),\n",
" genre: str = Form(None),\n",
" price: str = Form(None),\n",
" description: str = Form(None),\n",
" post_medias: Optional[List[str]] = Form(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" update_doc = {}\n",
"\n",
" if title is not None:\n",
" update_doc[\"title\"] = safe_strip(title)\n",
" update_doc[\"url\"] = transliterate(f\"{safe_strip(title)} {safe_strip(author)} {safe_strip(city)}\")\n",
" if author is not None:\n",
" update_doc[\"author\"] = safe_strip(author)\n",
" if grade is not None:\n",
" update_doc[\"grade\"] = safe_strip(grade)\n",
" if city is not None:\n",
" update_doc[\"city\"] = safe_strip(city)\n",
" if size is not None:\n",
" update_doc[\"size\"] = safe_strip(size)\n",
" if material is not None:\n",
" update_doc[\"material\"] = safe_strip(material)\n",
" if genre is not None:\n",
" update_doc[\"genre\"] = safe_strip(genre)\n",
" if price is not None:\n",
" update_doc[\"price\"] = safe_strip(price)\n",
" if description is not None:\n",
" update_doc[\"description\"] = safe_strip(description)\n",
" if post_medias:\n",
" update_doc[\"medias_original\"] = post_medias\n",
"\n",
" res = await posts_collection.find_one_and_update(\n",
" {'_id': ObjectId(post_id)}, \n",
" {'$set': update_doc},\n",
" return_document=False\n",
" )\n",
"\n",
" links_media = list(zip(res.get('medias_original',[]), res.get('medias',[])))\n",
"\n",
" if res is None:\n",
" raise HTTPException(status_code=404, detail=\"Картина не найдена\")\n",
"\n",
" doc_out = {\n",
" \"_id\":post_id,\n",
" \"id\":post_id,\n",
" **{k: v for k, v in update_doc.items()}\n",
" }\n",
" doc_out = bson_clear(doc_out)\n",
" \n",
" background_tasks.add_task(update_social_post, doc_out)\n",
" background_tasks.add_task(replace_new_medias, bson_clear(doc_out), links_media)\n",
" return doc_out\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "3f18f6a6",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/posts/media/chunk\")\n",
"async def upload_chunk(\n",
" chunk: UploadFile = File(...),\n",
" file_id: str = Form(...),\n",
" chunk_index: int = Form(...),\n",
" total_chunks: int = Form(...),\n",
" filename: str = Form(...),\n",
" content_type: str = Form(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" try:\n",
" data = await chunk.read()\n",
" b64_data = base64.b64encode(data).decode(\"ascii\")\n",
"\n",
" key = f\"upload:{file_id}\"\n",
"\n",
" # Store meta only once\n",
" meta = await redis_client.hgetall(key)\n",
" if not meta:\n",
" await redis_client.hset(key, mapping={\n",
" \"filename\": filename,\n",
" \"content_type\": content_type,\n",
" \"total_chunks\": str(total_chunks),\n",
" \"user_id\": str(current_user.get(\"id\", \"\")),\n",
" })\n",
" await redis_client.expire(key, CHUNK_TTL)\n",
"\n",
" # Store the chunk\n",
" await redis_client.hset(key, f\"chunk:{chunk_index}\", b64_data)\n",
" await redis_client.expire(key, CHUNK_TTL)\n",
"\n",
" return {\"ok\": True, \"received\": chunk_index}\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))\n",
"\n",
"\n",
"@app.post(\"/api/posts/media/finalize\")\n",
"async def finalize_upload(\n",
" payload: dict,\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" file_id = payload.get(\"file_id\")\n",
" if not file_id:\n",
" raise HTTPException(status_code=400, detail=\"file_id required\")\n",
"\n",
" key = f\"upload:{file_id}\"\n",
" meta = await redis_client.hgetall(key)\n",
"\n",
" if not meta:\n",
" raise HTTPException(status_code=400, detail=\"Unknown or expired file_id\")\n",
"\n",
" total_chunks = int(meta[\"total_chunks\"])\n",
" filename = meta[\"filename\"]\n",
" content_type = meta[\"content_type\"]\n",
"\n",
" # Collect all chunks\n",
" chunks = []\n",
" for i in range(total_chunks):\n",
" b64 = await redis_client.hget(key, f\"chunk:{i}\")\n",
" if b64 is None:\n",
" raise HTTPException(status_code=400, detail=f\"Missing chunk {i}\")\n",
" chunks.append(base64.b64decode(b64))\n",
"\n",
" full_content = b\"\".join(chunks)\n",
"\n",
" temp_file = StarletteUploadFile(\n",
" file=BytesIO(full_content),\n",
" filename=filename,\n",
" headers={\"content-type\": content_type}\n",
" )\n",
"\n",
" try:\n",
" saved_name = await save_to_gridfs(temp_file, \"posts\")\n",
" return {\"post_medias\": [saved_name]}\n",
" finally:\n",
" # Cleanup\n",
" await redis_client.delete(key)\n",
"\n",
"\n",
"# Optional fallback (non-chunked)\n",
"@app.post(\"/api/posts/media\", response_model=dict)\n",
"async def post_media(\n",
" medias: Optional[List[UploadFile]] = File(None),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:posts\"])\n",
"):\n",
" try:\n",
" post_medias = await asyncio.gather(\n",
" *[save_to_gridfs(media, \"posts\") for media in medias or []]\n",
" )\n",
" return {\"post_medias\": post_medias}\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "9c463130",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/news\", response_model=dict)\n",
"async def post_news(\n",
" background_tasks: BackgroundTasks,\n",
" title: str = Form(...),\n",
" description: str = Form(None),\n",
" short_description: str = Form(None),\n",
" publish_on_platform: bool = Form(False),\n",
" post_medias: Optional[List[str]] = Form(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:news\"])\n",
"):\n",
" try:\n",
" doc = {\n",
" \"title\": safe_strip(title),\n",
" \"short_description\": safe_strip(short_description), \n",
" \"publish_on_platform\": publish_on_platform,\n",
" \"description\": safe_strip(description),\n",
" \"media\": post_medias,\n",
" \"media_original\": post_medias,\n",
" \"user\": display_user(current_user),\n",
" **{i:[] for i in SOCIALS},\n",
" }\n",
"\n",
" res = await news_collection.insert_one(doc)\n",
" doc_out = {\n",
" \"id\": str(res.inserted_id),\n",
" **{k: v for k, v in doc.items()}\n",
" }\n",
" doc_out = bson_clear(doc_out)\n",
" # background_tasks.add_task(email_new_post, doc_out)\n",
" background_tasks.add_task(replace_news_medias, doc_out)\n",
" return doc_out\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "de77638d",
"metadata": {},
"outputs": [],
"source": [
"@app.put(\"/api/news/{news_id}\", response_model=dict)\n",
"async def put_news(\n",
" background_tasks: BackgroundTasks,\n",
" news_id: str,\n",
" title: str = Form(...),\n",
" description: str = Form(None),\n",
" short_description: str = Form(None),\n",
" publish_on_platform: bool = Form(False),\n",
" post_medias: Optional[List[str]] = Form(...),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:news\"])\n",
"):\n",
" update_doc = {}\n",
"\n",
" if title is not None:\n",
" update_doc[\"title\"] = safe_strip(title)\n",
" if description is not None:\n",
" update_doc[\"description\"] = safe_strip(description)\n",
" if short_description is not None:\n",
" update_doc[\"short_description\"] = safe_strip(short_description)\n",
" if publish_on_platform is not None:\n",
" update_doc[\"publish_on_platform\"] = publish_on_platform\n",
" if post_medias:\n",
" update_doc[\"media\"] = post_medias\n",
" update_doc[\"media_original\"] = post_medias\n",
"\n",
" res = await news_collection.update_one(\n",
" {'_id': ObjectId(news_id)}, \n",
" {'$set': update_doc}\n",
" )\n",
"\n",
" if res.modified_count == 0:\n",
" raise HTTPException(status_code=404, detail=\"Картина не найдена\")\n",
"\n",
" doc_out = {\n",
" \"id\":news_id,\n",
" **{k: v for k, v in update_doc.items()}\n",
" }\n",
" doc_out = bson_clear(doc_out)\n",
" return doc_out\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "abe852d2",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/news/media\", response_model=dict)\n",
"async def news_media(\n",
" medias: Optional[List[UploadFile]] = File(None),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:news\"])\n",
"):\n",
" try:\n",
" media = await asyncio.gather(*[save_to_gridfs(media, 'news') for media in medias])\n",
" return {'media': media}\n",
" except Exception as e:\n",
" raise HTTPException(status_code=500, detail=str(e))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d0a9730e",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/read/notify\", response_model=Dict)\n",
"async def read_notify(\n",
" user: Dict = Security(get_user)\n",
"):\n",
" await notify_collection.update_many({'user_id':user.get('_id')},{'$set':{'read':False}})\n",
" return {}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "0479dfcd",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/competition\", response_model=Dict, tags=[\"Competition\", \"Partner\"])\n",
"async def post_competition(\n",
" background_tasks: BackgroundTasks,\n",
" payload: CompetitionCreate = Depends(build_payload),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"]),\n",
"):\n",
" try:\n",
" poster_id = await save_to_gridfs(payload.poster, \"competition\")\n",
" if payload.rules:\n",
" rules_id = await save_to_gridfs(payload.rules, \"competition\")\n",
" else:\n",
" rules_id = None\n",
"\n",
" await users_collection.update_one(\n",
" {\"_id\": ObjectId(current_user[\"_id\"])},\n",
" {\"$addToSet\": {\"send_address\": safe_strip(payload.address)}},\n",
" upsert=True,\n",
" )\n",
"\n",
" doc = {\n",
" \"url\": transliterate(safe_strip(payload.title)),\n",
" \"title\": safe_strip(payload.title),\n",
" \"topic\": safe_strip(payload.topic) if payload.topic else None,\n",
" \"type_\": payload.type_,\n",
" \"datetime_start\": payload.datetime_start,\n",
" \"date_finish\": payload.date_finish,\n",
" \"nominations\": payload.nominations,\n",
" \"address\": safe_strip(payload.address),\n",
" \"emailsNotify\": payload.emailsNotify,\n",
" \"serviceSocial\": payload.serviceSocial,\n",
" \"userSocial\": payload.userSocial,\n",
" \"short_desc\": safe_strip(payload.short_desc) if payload.short_desc else None,\n",
" \"poster_file_id\": poster_id,\n",
" \"poster_orig_id\": poster_id,\n",
" \"rules_file_id\": rules_id,\n",
" \"rules_link\": payload.rules_link,\n",
" \"status\": 0,\n",
" \"user\": display_user(current_user),\n",
" \"min_paintings\": payload.min_paintings,\n",
" \"max_paintings\": payload.max_paintings,\n",
" **{i:[] for i in SOCIALS},\n",
" }\n",
"\n",
" res = await competitions_col.insert_one(doc)\n",
"\n",
" background_tasks.add_task(\n",
" poster_competition, bson_clear({**doc, \"_id\": res.inserted_id})\n",
" )\n",
"\n",
" return {\"ok\": True, \"competition_id\": str(res.inserted_id)}\n",
" except Exception as exc:\n",
" logging.error(str(exc))\n",
" raise HTTPException(status_code=500, detail=str(exc))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "cc6f3f8b",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/competition/publish\", response_model=Dict, tags=[\"Competition\", \"Partner\"])\n",
"async def publish_competition(\n",
" background_tasks: BackgroundTasks,\n",
" payload: CompetitionPublishParams = Depends(build_publish_payload),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"]),\n",
"):\n",
" try:\n",
" logging.info(\n",
" {\n",
" \"current_user\": current_user.get(\"_id\"),\n",
" \"competition_id\": payload.competition_id,\n",
" \"emailsNotify\": payload.emailsNotify,\n",
" \"serviceSocial\": payload.serviceSocial,\n",
" \"userSocial\": payload.userSocial,\n",
" \"usersSocial\": payload.usersSocial,\n",
" }\n",
" )\n",
"\n",
" competition = await competitions_col.find_one(\n",
" {\n",
" \"user._id\": current_user.get(\"_id\"),\n",
" \"_id\": ObjectId(payload.competition_id),\n",
" }\n",
" )\n",
" if not competition:\n",
" raise HTTPException(status_code=404, detail=\"Конкурс не найден\")\n",
"\n",
" competition = bson_clear(competition)\n",
"\n",
" if competition[\"datetime_start\"] > datetime.now():\n",
" await competitions_col.update_one(\n",
" {\"_id\": ObjectId(payload.competition_id)},\n",
" {\"$set\": {\"datetime_start\": datetime.now()}},\n",
" )\n",
"\n",
" if payload.emailsNotify:\n",
" background_tasks.add_task(invite_users_to_competitions, competition)\n",
"\n",
" if any([payload.serviceSocial, payload.userSocial, payload.usersSocial]):\n",
" background_tasks.add_task(\n",
" service_social_competition,\n",
" competition,\n",
" payload.serviceSocial,\n",
" payload.userSocial,\n",
" payload.usersSocial,\n",
" )\n",
"\n",
" return {\"ok\": True, \"competition_id\": payload.competition_id}\n",
" except Exception as exc:\n",
" logging.error(str(exc))\n",
" raise HTTPException(status_code=500, detail=str(exc))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "80c70929",
"metadata": {},
"outputs": [],
"source": [
"@app.put(\"/api/competition/{competition_id}\", response_model=Dict, tags=[\"Competition\", \"Partner\"])\n",
"async def put_competition(\n",
" background_tasks: BackgroundTasks,\n",
" competition_id: str,\n",
" payload: CompetitionUpdate = Depends(build_update_payload),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"]),\n",
"):\n",
" try:\n",
" update_doc = {\n",
" \"user\": display_user(current_user),\n",
" \"rules_link\": payload.rules_link,\n",
" \"type_\": payload.type_,\n",
" }\n",
" if payload.nominations is not None:\n",
" update_doc[\"nominations\"] = payload.nominations\n",
" if payload.title is not None:\n",
" update_doc[\"title\"] = safe_strip(payload.title)\n",
" if payload.topic is not None:\n",
" update_doc[\"topic\"] = safe_strip(payload.topic)\n",
" if payload.datetime_start is not None:\n",
" update_doc[\"datetime_start\"] = payload.datetime_start\n",
" if datetime.now(timezone.utc) + timedelta(hours=3) < payload.datetime_start:\n",
" update_doc[\"status\"] = -1\n",
" if payload.date_finish is not None:\n",
" update_doc[\"date_finish\"] = payload.date_finish\n",
" if payload.address is not None:\n",
" await users_collection.update_one(\n",
" { \"_id\": ObjectId(current_user[\"_id\"]) },\n",
" { \"$addToSet\": { \"send_address\": safe_strip(payload.address) } },\n",
" upsert=True\n",
" )\n",
" update_doc[\"address\"] = safe_strip(payload.address)\n",
" if payload.short_desc is not None:\n",
" update_doc[\"short_desc\"] = safe_strip(payload.short_desc)\n",
" if payload.emailsNotify is not None:\n",
" update_doc[\"emailsNotify\"] = payload.emailsNotify\n",
" if payload.serviceSocial is not None:\n",
" update_doc[\"serviceSocial\"] = payload.serviceSocial\n",
" if payload.userSocial is not None:\n",
" update_doc[\"userSocial\"] = payload.userSocial\n",
" if payload.min_paintings is not None:\n",
" update_doc[\"min_paintings\"] = int(payload.min_paintings)\n",
" if payload.max_paintings is not None:\n",
" update_doc[\"max_paintings\"] = int(payload.max_paintings)\n",
" if payload.poster:\n",
" poster_id = await save_to_gridfs(payload.poster, 'competition')\n",
" update_doc[\"poster_file_id\"] = poster_id\n",
" background_tasks.add_task(poster_competition, bson_clear({**update_doc, '_id': competition_id}))\n",
" if payload.rules:\n",
" rules_id = await save_to_gridfs(payload.rules, 'competition')\n",
" update_doc[\"rules_file_id\"] = rules_id\n",
"\n",
" res = await competitions_col.update_one(\n",
" {'_id': ObjectId(competition_id)}, \n",
" {'$set': update_doc}\n",
" )\n",
"\n",
" return {'ok': True}\n",
" except Exception as exc:\n",
" logging.error(str(exc))\n",
" raise HTTPException(status_code=500, detail=str(exc))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "c04c5888",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/api/exhibitions\", response_model=List)\n",
"async def get_exhibitions():\n",
" exhibitions = []\n",
" current_time = datetime.now(timezone.utc) + timedelta(hours=3)\n",
"\n",
" async for doc in exhibition_collection.find({\n",
" '$or': [\n",
" {'datetime_start': {'$lt': current_time}},\n",
" ],\n",
" 'prod': True\n",
" },{'vk': 0,'max': 0,'tg': 0,'fb': 0,'inst': 0}).sort('_id',-1).limit(1000):\n",
" exhibitions.append(bson_clear(doc))\n",
" return exhibitions"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "55c76bf4",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/exhibition\", response_model=Dict)\n",
"async def post_exhibition(\n",
" background_tasks: BackgroundTasks,\n",
" payload: ExhibitionCreate = Depends(build_exhibition_payload),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" try:\n",
" poster_id = await save_to_gridfs(payload.poster, 'exhibition')\n",
" rules_id = await save_to_gridfs(payload.rules, 'exhibition') if payload.rules else None\n",
" tizer_id = await save_to_gridfs(payload.tizer, 'exhibition') if payload.tizer else None\n",
"\n",
" await users_collection.update_one(\n",
" {\"_id\": ObjectId(current_user[\"_id\"])},\n",
" {\"$addToSet\": {\n",
" \"send_address\": safe_strip(payload.address),\n",
" \"places\": safe_strip(payload.place)\n",
" }},\n",
" upsert=True\n",
" )\n",
" \n",
" doc = {\n",
" \"url\": transliterate(safe_strip(payload.title)),\n",
" \"title\": safe_strip(payload.title),\n",
" \"topic\": safe_strip(payload.topic) if payload.topic else None,\n",
" \"datetime_start\": payload.datetime_start,\n",
" \"date_finish\": payload.date_finish,\n",
" \"address\": safe_strip(payload.address) if payload.address else None,\n",
" \"place\": safe_strip(payload.place) if payload.place else None,\n",
" \"rules_file_id\": rules_id,\n",
" \"rules_link\": payload.rules_link,\n",
" \"tizer_file_id\": tizer_id,\n",
" \"tizer_link\": payload.tizer_link,\n",
" \n",
" \"poster_orig_id\": poster_id,\n",
" \"poster_file_id\": poster_id,\n",
" \"status\": 0,\n",
" \"prod\": PROD,\n",
" \"user\": display_user(current_user),\n",
" \"posts\": [],\n",
" **{i:[] for i in SOCIALS},\n",
" }\n",
" \n",
" res = await exhibition_collection.insert_one(doc)\n",
" exhibition_id = res.inserted_id\n",
" background_tasks.add_task(\n",
" poster_exhibition, \n",
" bson_clear({**doc, '_id': exhibition_id})\n",
" )\n",
" \n",
" return {'_id': str(exhibition_id)}\n",
" except Exception as e:\n",
" logging.error(str(e))\n",
" raise HTTPException(status_code=500, detail=str(e))"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "46e76a8c",
"metadata": {},
"outputs": [],
"source": [
"@app.put(\"/api/exhibition/{exhibition_id}\", response_model=Dict)\n",
"async def put_exhibition(\n",
" background_tasks: BackgroundTasks,\n",
" exhibition_id: str,\n",
" payload: ExhibitionUpdate = Depends(build_update_exhibition_payload),\n",
" current_user: Dict = Security(get_current_user, scopes=[\"create:super_items\"])\n",
"):\n",
" try:\n",
" update_doc = {\n",
" \"user\": display_user(current_user),\n",
" }\n",
"\n",
" if payload.title is not None:\n",
" update_doc[\"title\"] = safe_strip(payload.title)\n",
" if payload.topic is not None:\n",
" update_doc[\"topic\"] = safe_strip(payload.topic)\n",
" if payload.place is not None:\n",
" await users_collection.update_one(\n",
" {\"_id\": ObjectId(current_user[\"_id\"])},\n",
" {\"$addToSet\": {\"places\": safe_strip(payload.place)}},\n",
" upsert=True\n",
" )\n",
" update_doc[\"place\"] = safe_strip(payload.place)\n",
" if payload.datetime_start is not None:\n",
" update_doc[\"datetime_start\"] = payload.datetime_start\n",
" if payload.date_finish is not None:\n",
" update_doc[\"date_finish\"] = payload.date_finish\n",
" if payload.address is not None:\n",
" await users_collection.update_one(\n",
" {\"_id\": ObjectId(current_user[\"_id\"])},\n",
" {\"$addToSet\": {\"send_address\": safe_strip(payload.address)}},\n",
" upsert=True\n",
" )\n",
" update_doc[\"address\"] = safe_strip(payload.address)\n",
" if payload.short_desc is not None:\n",
" update_doc[\"short_desc\"] = safe_strip(payload.short_desc)\n",
" update_doc[\"rules_link\"] = payload.rules_link\n",
" update_doc[\"tizer_link\"] = payload.tizer_link\n",
"\n",
" # Обработка файлов\n",
" if payload.poster:\n",
" poster_id = await save_to_gridfs(payload.poster, 'exhibition')\n",
" update_doc[\"poster_file_id\"] = poster_id\n",
" update_doc[\"poster_orig_id\"] = poster_id\n",
" background_tasks.add_task(\n",
" poster_exhibition, \n",
" bson_clear({**update_doc, '_id': exhibition_id})\n",
" )\n",
" if payload.tizer:\n",
" tizer_id = await save_to_gridfs(payload.tizer, 'exhibition')\n",
" update_doc[\"tizer_file_id\"] = tizer_id\n",
" update_doc[\"tizer_link\"] = None\n",
" if payload.rules:\n",
" rules_id = await save_to_gridfs(payload.rules, 'exhibition')\n",
" update_doc[\"rules_file_id\"] = rules_id\n",
" update_doc[\"rules_link\"] = None\n",
"\n",
" # Если нужно добавить nominations / min_paintings и т.д. — расширь модель и этот блок\n",
"\n",
" res = await exhibition_collection.update_one(\n",
" {'_id': ObjectId(exhibition_id)}, \n",
" {'$set': update_doc}\n",
" )\n",
"\n",
" return {'_id': exhibition_id}\n",
" except Exception as e:\n",
" logging.error(str(e))\n",
" raise HTTPException(status_code=500, detail=str(e))"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "f1412592",
"metadata": {},
"outputs": [],
"source": [
"async def stream_gridfs_file(file_id: str, bucket_name: str):\n",
" if not file_id:\n",
" return None\n",
"\n",
" grid = gridfs_map.get(bucket_name)\n",
" if grid is None:\n",
" return None\n",
"\n",
" try:\n",
" oid = ObjectId(file_id)\n",
" except Exception:\n",
" return None\n",
"\n",
" stream = None\n",
" try:\n",
" stream = await grid.open_download_stream(oid)\n",
"\n",
" filename = stream.filename or \"file\"\n",
" content_type = (\n",
" stream.content_type\n",
" or mimetypes.guess_type(filename)[0]\n",
" or \"application/octet-stream\"\n",
" )\n",
" file_length = getattr(stream, \"length\", None)\n",
"\n",
" async def file_iterator(chunk_size: int = 64 * 1024):\n",
" try:\n",
" while True:\n",
" chunk = await stream.read(chunk_size)\n",
" if not chunk:\n",
" break\n",
" yield chunk\n",
" finally:\n",
" if stream is not None:\n",
" close = getattr(stream, \"close\", None)\n",
" if asyncio.iscoroutinefunction(close) or asyncio.iscoroutine(close):\n",
" await close()\n",
" elif callable(close):\n",
" close()\n",
"\n",
" return filename, content_type, file_length, file_iterator\n",
"\n",
" except Exception:\n",
" if stream is not None:\n",
" close = getattr(stream, \"close\", None)\n",
" if asyncio.iscoroutinefunction(close) or asyncio.iscoroutine(close):\n",
" await close()\n",
" elif callable(close):\n",
" close()\n",
" return None\n",
"\n",
"\n",
"def content_disposition_header(filename: str, inline: bool = True) -> str:\n",
" dispo_type = \"inline\" if inline else \"attachment\"\n",
"\n",
" fallback = filename.encode(\"latin-1\", errors=\"ignore\").decode(\"latin-1\")\n",
" fallback = fallback.replace('\"', '\\\\\"').replace(\"\\\\\", \"\\\\\\\\\") or \"file\"\n",
"\n",
" try:\n",
" filename.encode(\"latin-1\")\n",
" return f'{dispo_type}; filename=\"{fallback}\"'\n",
" except UnicodeEncodeError:\n",
" filename_quoted = quote(filename, safe=\"\")\n",
" return f'{dispo_type}; filename=\"{fallback}\"; filename*=UTF-8\\'\\'{filename_quoted}'\n",
"\n",
"\n",
"@app.get(\"/b/{bucket_name}/{file_id}\")\n",
"async def get_file(bucket_name: str, file_id: str):\n",
" res = await stream_gridfs_file(file_id, bucket_name)\n",
" if res is None:\n",
" raise HTTPException(status_code=404, detail=\"File not found\")\n",
" filename, content_type, file_length, iterator = res\n",
" headers = {\n",
" \"Content-Disposition\": content_disposition_header(filename, inline=True),\n",
" \"Cache-Control\": \"public, max-age=3600\",\n",
" \"ETag\": f'\"{bucket_name}:{file_id}\"',\n",
" \"Vary\": \"Accept-Encoding\",\n",
" }\n",
" if file_length is not None:\n",
" headers[\"Content-Length\"] = str(file_length)\n",
" return StreamingResponse(iterator(), media_type=content_type, headers=headers)\n",
"\n",
"\n",
"@app.get(\"/a/{user_id}\")\n",
"async def get_avatar(user_id: str):\n",
" user = await users_collection.find_one({'_id': ObjectId(user_id)},{'img':1})\n",
" file_id = user['img']\n",
" res = await stream_gridfs_file(file_id, 'users')\n",
" if res is None:\n",
" raise HTTPException(status_code=404, detail=\"File not found\")\n",
" filename, content_type, file_length, iterator = res\n",
" headers = {\n",
" \"Content-Disposition\": content_disposition_header(filename, inline=True),\n",
" \"Cache-Control\": \"public, max-age=3600\",\n",
" \"ETag\": f'\"users:{file_id}\"',\n",
" \"Vary\": \"Accept-Encoding\",\n",
" }\n",
" if file_length is not None:\n",
" headers[\"Content-Length\"] = str(file_length)\n",
" return StreamingResponse(iterator(), media_type=content_type, headers=headers)\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "cdf3993c",
"metadata": {},
"outputs": [],
"source": [
"async def unsubscribe_user(\n",
" email: str, \n",
" token: Optional[str] = None\n",
") -> bool:\n",
" try:\n",
" if token:\n",
" stored_email = await redis_client.get(f\"unsub:{token}\")\n",
" if not stored_email or stored_email != email:\n",
" return False\n",
" await redis_client.delete(f\"unsub:{token}\")\n",
"\n",
" result = await mongo_client['email']['tokens'].update_one(\n",
" {\"token\": token, \"email\": email, \"used\": False},\n",
" {\"$set\": {\"used\": True, \"used_at\": datetime.utcnow()}}\n",
" )\n",
" if result.modified_count == 0:\n",
" return False\n",
"\n",
" await users_collection.update_one(\n",
" {\"email\": email},\n",
" {\"$set\": {\n",
" \"email_subscribed\": False,\n",
" \"email_unsubscribed_at\": datetime.utcnow(),\n",
" \"email_unsubscribe_reason\": \"user_request\"\n",
" }}\n",
" )\n",
"\n",
" logging.info(f\"✅ Пользователь отписался: {email}\")\n",
" return True\n",
"\n",
" except Exception as e:\n",
" logging.error(f\"❌ Ошибка отписки {email}: {e}\")\n",
" return False\n",
"\n",
"\n",
"@app.get(\"/api/v1/email/unsubscribe\", response_class=HTMLResponse)\n",
"async def unsubscribe(\n",
" request: Request,\n",
" email: EmailStr,\n",
" token: Optional[str] = None,\n",
"):\n",
" logging.info(f\"Отписка {token} {email}\")\n",
" await unsubscribe_user(email)\n",
" \n",
" return \"\"\"\n",
" <html>\n",
" <head><title>Отписка</title></head>\n",
" <body style=\"font-family:Arial;text-align:center;margin-top:50px;\">\n",
" <h2>✅ Вы успешно отписались</h2>\n",
" <p>Вы больше не будете получать письма от нас.</p>\n",
" <p><a href=\"/\">Вернуться на сайт</a></p>\n",
" </body>\n",
" </html>\n",
" \"\"\"\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "32612dc5",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/admin/users\", response_class=HTMLResponse)\n",
"async def profile_admin_statistics(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"admin\"])\n",
"):\n",
" users = [doc async for doc in users_collection.find().sort({'_id':-1})]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/admin/users.html\", context={\"user\": user,\n",
" 'users': users \n",
" }\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "caf0fa14",
"metadata": {},
"outputs": [],
"source": [
"from collections import defaultdict\n",
"@app.get(\"/profile/admin/statistics\", response_class=HTMLResponse)\n",
"async def profile_admin_statistics(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user, scopes=[\"admin\"])\n",
"):\n",
" pipeline1 = [\n",
" {\"$match\": {\"roles\": \"user\"}}, \n",
" {\n",
" \n",
" \"$group\": {\n",
" \"_id\": \"$living_place\",\n",
" \"user_count\": {\"$sum\": 1},\n",
" \"user_ids\": {\"$push\": \"$_id\"}\n",
" }\n",
" },\n",
" {\"$sort\": {\"user_count\": -1}}\n",
" ]\n",
"\n",
" living_stats = await users_collection.aggregate(pipeline1).to_list(length=None)\n",
"\n",
"\n",
" for stat in living_stats:\n",
" if stat[\"_id\"]:\n",
" user_ids = stat[\"user_ids\"] # list of user _id values (ObjectId or str)\n",
" # If posts store user._id as string, convert ObjectId -> str\n",
" user_id_strings = [str(u) for u in user_ids]\n",
"\n",
" # post_count (use count_documents as you had)\n",
" stat[\"post_count\"] = await posts_collection.count_documents(\n",
" {\"user._id\": {\"$in\": user_id_strings}}\n",
" )\n",
"\n",
" # usernames\n",
" cursor = users_collection.find(\n",
" {\"_id\": {\"$in\": user_ids}},\n",
" {\"username\": 1, \"_id\": 1}\n",
" ).limit(1000)\n",
" stat[\"usernames\"] = await cursor.to_list(length=None)\n",
"\n",
" # fetch posts for these users (limit/fields optional)\n",
" posts_cursor = posts_collection.find(\n",
" {\"user._id\": {\"$in\": user_id_strings}},\n",
" {\"_id\": 1, \"title\": 1, \"created_at\": 1, \"user._id\": 1}\n",
" ).sort(\"created_at\", -1)\n",
" stat[\"posts\"] = await posts_cursor.to_list(length=None)\n",
"\n",
" pipeline2 = [\n",
" {\"$match\": {\"roles\": \"user\"}},\n",
" {\n",
" \"$project\": {\n",
" \"year_month\": {\n",
" \"$dateToString\": {\n",
" \"format\": \"%Y-%m\",\n",
" \"date\": {\"$toDate\": \"$_id\"}\n",
" }\n",
" }\n",
" }\n",
" },\n",
" {\n",
" \"$group\": {\n",
" \"_id\": \"$year_month\",\n",
" \"user_count\": {\"$sum\": 1},\n",
" \"user_ids\": {\"$push\": \"$_id\"}\n",
" }\n",
" },\n",
" {\"$sort\": {\"_id\": -1}}\n",
" ]\n",
"\n",
" reg_stats = await users_collection.aggregate(pipeline2).to_list(length=None)\n",
"\n",
" for stat in reg_stats:\n",
" user_ids = stat[\"user_ids\"] # list of user _id values (ObjectId or str)\n",
" user_id_strings = [str(u) for u in user_ids] # use this if posts.user._id is a str\n",
"\n",
" stat[\"post_count\"] = await posts_collection.count_documents(\n",
" {\"user._id\": {\"$in\": user_id_strings}}\n",
" )\n",
"\n",
" cursor = users_collection.find(\n",
" {\"_id\": {\"$in\": user_ids}},\n",
" {\"username\": 1, \"_id\": 0}\n",
" ).limit(1000)\n",
" stat[\"usernames\"] = await cursor.to_list(length=None)\n",
"\n",
" # fetch posts (flat list)\n",
" posts_cursor = posts_collection.find(\n",
" {\"user._id\": {\"$in\": user_id_strings}},\n",
" {\"_id\": 1, \"title\": 1, \"created_at\": 1, \"user._id\": 1}\n",
" ).sort(\"created_at\", -1)\n",
" stat[\"posts\"] = await posts_cursor.to_list(length=None)\n",
"\n",
" # OR: group posts per user\n",
" posts = stat[\"posts\"]\n",
" grouped = defaultdict(list)\n",
" for p in posts:\n",
" grouped[p[\"user\"][\"_id\"]].append(p)\n",
" stat[\"posts_by_user\"] = dict(grouped)\n",
"\n",
"\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/admin/statistics.html\", context={\"user\": user, \"living_stats\": living_stats,\n",
" \"reg_stats\": reg_stats,\n",
" \"all_users\": await users_collection.count_documents({ 'roles': \"user\" }),\n",
" \"all_city\": len(await users_collection.distinct(\"living_place\", filter={ 'roles': \"user\" }))\n",
" }\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "afe4a444",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/tg\")\n",
"async def tg_result_save(data:dict, token: str = Security(get_token)):\n",
" item = data.get('item')\n",
" result = data.get('result', {})\n",
" if item:\n",
" update_spec = {\"$push\": {\"tg\": result}}\n",
" await mongo_client[item.get('db')][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec)"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d78c34a3",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/social\", response_class=HTMLResponse)\n",
"async def profile_social(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" socials = [doc async for doc in social_collection.find({'user_id': str(user.get('_id'))})]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/social.html\", context={\"user\": user, 'socials': socials}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5ace34ae",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/invitation/{invitation_id}\", response_class=HTMLResponse)\n",
"async def profile_invitation(\n",
" request: Request,\n",
" invitation_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" data = await invitations_collection.find_one({'_id': ObjectId(invitation_id)})\n",
" match_ = {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'prod': PROD,\n",
" }\n",
" if data.get('competition'):\n",
" match_.update({'competition_id': data.get('competition').get('_id')})\n",
" if data.get('exhibition_id'):\n",
" match_.update({'exhibition_id': data.get('exhibition_id')})\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": match_},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" posts = [doc async for doc in cursor]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/invitation.html\", context={ \"user\": user, \"doc\": data, \"data\": posts}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "2d516c27",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/invitation\", response_class=HTMLResponse)\n",
"async def profile_invitation(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" data = await invitations_collection.find({'prod': PROD,'user_id': user.get('_id')}).sort('_id', -1).to_list()\n",
" await notify_collection.update_many({'user_id':user.get('_id'),'type':'invitation'},{\"$set\":{'read':False}})\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/invitations.html\", context={ \"user\": user, \"data\": data, \"now\": datetime.now()}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "140fc75b",
"metadata": {},
"outputs": [],
"source": [
"@app.post('/api/social/settings', response_model=dict)\n",
"async def social_setting(\n",
" id: Optional[str] = Body(None, embed=True), \n",
" key: Optional[str] = Body(None, embed=True),\n",
" check: Optional[bool] = Body(None, embed=True),\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" await social_collection.update_one({'_id': ObjectId(id), 'user_id': user.get('_id')},{'$set': {key:check}})\n",
" return {'ok': True}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6df17366",
"metadata": {},
"outputs": [],
"source": [
"async def get_delivery_post(post_id):\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" 'post_id': post_id,\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" return data[0] if data else False"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "028d0310",
"metadata": {},
"outputs": [],
"source": [
"@app.post('/api/v1/views')\n",
"async def get_views_count(request: Request):\n",
" url = (await request.json())['url']\n",
" if 'artist' in url:\n",
" user_id = url.split('/')[-1]\n",
" total_key = f\"total_w_c_artists:{user_id}\"\n",
" today_key = f\"today_w_c_artists:{user_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
" return {\"totalViews\":total_watch_count,\"todayViews\":today_watch_count}\n",
" if 'post' in url:\n",
" paint_id = url.split('/')[-1]\n",
" total_key = f\"total_watch_count_paint:{paint_id}\"\n",
" today_key = f\"today_watch_count_paint:{paint_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
" return {\"totalViews\":total_watch_count,\"todayViews\":today_watch_count}\n",
" if 'news' in url: \n",
" news_id = url.split('/')[-1]\n",
" total_key = f\"total_watch_count_news:{news_id}\"\n",
" today_key = f\"today_watch_count_news:{news_id}\"\n",
" total_watch_count = await redis_client.incr(total_key)\n",
" today_date = datetime.now().strftime(\"%Y-%m-%d\")\n",
" daily_key = f\"{today_key}:{today_date}\"\n",
" today_watch_count = await redis_client.incr(daily_key)\n",
" return {\"totalViews\":total_watch_count,\"todayViews\":today_watch_count}\n",
" return {\"totalViews\":0,\"todayViews\":0}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "70e57454",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/api/v1/generic/post', response_model=dict)\n",
"async def get_posts(\n",
" per_page: Optional[int] = Query(4),\n",
" page: Optional[int] = Query(1),\n",
" user_id: Optional[str] = Query(None),\n",
" filter: Optional[str] = Query(None, description=\"JSON filter string or omitted\")\n",
"):\n",
" posts = [] \n",
" if filter:\n",
" try:\n",
" filt = json.loads(filter)\n",
" mql = convert_to_mql(filt)\n",
" def mql_to_mongo(m):\n",
" if \"and\" in m:\n",
" return {\"$and\": [mql_to_mongo(x) for x in m[\"and\"]]}\n",
" if \"or\" in m:\n",
" return {\"$or\": [mql_to_mongo(x) for x in m[\"or\"]]}\n",
" field, op, value = m[\"field\"], m[\"op\"], m[\"value\"]\n",
" if field == 'author_name':\n",
" field = 'author'\n",
" if field == 'user.gallery_name':\n",
" field = 'user.username'\n",
" if field == 'name':\n",
" field = 'title'\n",
" if op == \"like\":\n",
" return {field: {\"$regex\": value, \"$options\": \"i\"}}\n",
" return {field: value}\n",
" mongo_query = mql_to_mongo(mql)\n",
" for post in [doc async for doc in posts_collection.find(mongo_query).sort({\"_id\":-1}).limit(per_page * page + 8)][per_page * page - per_page:per_page * page]:\n",
" post['delivery'] = await get_delivery_post(str(post.get('_id')))\n",
" post['user']['avatar_url'] = f\"/a/{post['user'].get('_id')}\"\n",
" post['user']['name'] = post['user']['username']\n",
" photos = [{'url':f\"/b/posts/{i}\", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias'])]\n",
" post['id'] = post.get('_id')\n",
" post['photos'] = photos\n",
" post['name'] = post['title']\n",
" post['author_name'] = post['author']\n",
" post['media'] = photos\n",
" posts.append(bson_clear(post))\n",
" post_count = await posts_collection.count_documents(mongo_query)\n",
" return {\n",
" 'current_page': page,\n",
" 'data':posts,\n",
" 'total': post_count,\n",
" 'to': 16,\n",
" 'last_page': post_count / per_page,\n",
" 'per_page': per_page\n",
" }\n",
" except json.JSONDecodeError:\n",
" pass\n",
" q = {}\n",
" \n",
" if user_id:\n",
" try:\n",
" if len(str(user_id)) < 6:\n",
" user_id = int(user_id)\n",
" user = await users_collection.find_one({'id':user_id})\n",
" q['user._id'] = str(user[\"_id\"])\n",
" else:\n",
" q['user._id'] = user_id\n",
" except:\n",
" q['user._id'] = user_id\n",
" for post in [doc async for doc in posts_collection.find(q).sort({\"_id\":-1}).limit(per_page * page + 8)][per_page * page - per_page:per_page * page]:\n",
" post['delivery'] = await get_delivery_post(str(post.get('_id')))\n",
" post['user']['avatar_url'] = f\"/a/{post['user'].get('_id')}\"\n",
" post['user']['name'] = post['user']['username']\n",
" post['author_name'] = post['author']\n",
" photos = [{'url':f\"/b/posts/{i}\", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias'])]\n",
" post['id'] = post.get('_id')\n",
" post['photos'] = photos\n",
" post['media'] = photos\n",
" post['name'] = post['title']\n",
" posts.append(bson_clear(post))\n",
" post_count = await posts_collection.count_documents(q)\n",
" return {\n",
" 'current_page': page,\n",
" 'data':posts,\n",
" 'total': post_count,\n",
" 'to': 16,\n",
" 'last_page': post_count / per_page,\n",
" 'per_page': per_page\n",
" }"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "955d38bc",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/api/v2/statistics', response_model=dict)\n",
"async def statistics_v2(\n",
" user: Optional[str] = Query(None),\n",
" post: Optional[str] = Query(None),\n",
"):\n",
" if user:\n",
" return {\"statistics\":await users_collection.count_documents({ 'roles': \"user\" }),\"additional_stats\":len(await users_collection.distinct(\"living_place\", filter={ 'roles': \"user\" }))}\n",
" else:\n",
" return {\"statistics\":await posts_collection.count_documents({}),\"additional_stats\":await users_collection.count_documents({ 'roles': \"user\" })}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "eecd50fe",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/api/v2/user/{user_id}', response_model=dict)\n",
"async def get_user_by_id(\n",
" user_id: Optional[str]\n",
"):\n",
" try:\n",
" user = await users_collection.find_one({'id': int(user_id)})\n",
" except:\n",
" user = await users_collection.find_one({'_id': ObjectId(user_id)})\n",
" timestamp = user.get('_id').generation_time\n",
" user['posts_count'] = await posts_collection.count_documents({'user._id':str(user.get('_id'))})\n",
" timestamp_str = timestamp.strftime(\"%Y-%m-%dT%H:%M:%S.%f\")[:-3] + \"Z\"\n",
" user['created_at'] = timestamp_str\n",
" user['avatar_url'] = f\"https://avtopost.kultmost.ru/a/{user.get('_id')}\"\n",
" user['name'] = user['username']\n",
" user.pop('hashed_password')\n",
" user.pop('phone')\n",
" user.pop('email')\n",
" return {'data':bson_clear(user)}"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "10385a18",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/api/v1/generic/post/{post_id}', response_model=dict)\n",
"async def get_post_by_id(\n",
" post_id: Optional[str]\n",
"):\n",
" try:\n",
" post = await posts_collection.find_one({'id': int(post_id)})\n",
" except:\n",
" post = await posts_collection.find_one({'_id': ObjectId(post_id)})\n",
" post['user']['avatar_url'] = f\"/a/{post['user'].get('_id')}\"\n",
" post['user']['name'] = post['user']['username']\n",
" photos = [{'url':f\"/b/posts/{i}\", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias_original'])]\n",
" post['photos'] = photos\n",
" post['id'] = post.get('_id')\n",
" post['name'] = post['title']\n",
" post['media'] = photos\n",
" return bson_clear(post)"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "06e7d5f2",
"metadata": {},
"source": [
"## Work"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13c5c5ab",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/invitation/competition/{competition_id}\", response_class=HTMLResponse)\n",
"async def profile_invitation(\n",
" request: Request,\n",
" competition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
" req = await competition_request_col.find_one({'competition_id': competition_id, 'user._id': user.get('_id')})\n",
" \n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/invitation/competition.html\", context={ \"user\": user, \"competition\": competition, \"req\": req}\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "cf6b2218",
"metadata": {},
"outputs": [],
"source": [
"@app.post('/api/invitation/accept/{invitations_id}')\n",
"async def invitation_accept(invitations_id: str, request: Request):\n",
" payload = await request.json()\n",
" posts_ids: List[str] = payload.get(\"posts\", [])\n",
"\n",
" # optional: валидация\n",
" if not isinstance(posts_ids, list):\n",
" posts_ids = []\n",
"\n",
" await accept_invitations(invitations_id, posts_ids)\n",
" return {'ok': True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "994a94e1",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/profile/delivery/{post_id}')\n",
"async def profile_delivery_history_post(request: Request, post_id:str, user: Dict = Security(get_current_user)):\n",
" data = await delivery_collection.find({'post_id': post_id, \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ]}).sort('_id', -1).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery_history.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"data\": data,\n",
" }\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6157e497",
"metadata": {},
"outputs": [],
"source": [
"# @app.get('/profile/delivery/{type_d}/{item_id}')\n",
"# async def profile_delivery_by_old(request: Request, type_d:str, item_id:str, user: Dict = Security(get_current_user)):\n",
"# cursor = delivery_collection.aggregate([\n",
"# {\"$match\": {\n",
"# \"$or\": [\n",
"# {\"sender._id\": user.get('_id')},\n",
"# {\"user._id\": user.get('_id')},\n",
"# {\"receiver._id\": user.get('_id')}\n",
"# ],\n",
"# f'{type_d}_id': ObjectId(item_id),\n",
"# 'prod': PROD,\n",
"# }},\n",
"# {\"$sort\": {\"_id\": -1}},\n",
"# {\"$group\": {\n",
"# \"_id\": \"$post_id\",\n",
"# \"doc\": {\"$first\": \"$$ROOT\"}\n",
"# }},\n",
"# {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
"# {\"$sort\": {\"_id\": -1}},\n",
"# ])\n",
"\n",
"# data = [doc async for doc in cursor]\n",
"# data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
"\n",
"# return templates.TemplateResponse(\n",
"# request=request,\n",
"# name=\"profile/delivery_old.html\",\n",
"# context={\n",
"# \"user\": user,\n",
"# \"data\": data,\n",
"# }\n",
"# )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "089ba597",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/profile/delivery/competition/{competition_id}')\n",
"async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
"\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery_old.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"data\": data,\n",
" }\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "2148ef21",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/profile/delivery/exhibition/{exhibition_id}')\n",
"async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, user: Dict = Security(get_current_user)):\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'exhibition_id': exhibition_id,\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
"\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery_old.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"data\": data,\n",
" }\n",
" )"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "96302958",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\n",
" \"/profile/delivery\",\n",
" response_class=HTMLResponse,\n",
" summary=\"Личный кабинет → Доставка Картин\",\n",
" description=\"\"\"\n",
" Отображает страницу с историей доставок пользователя.\n",
" \n",
" Показывает **все** доставки, где пользователь является либо отправителем (`sender`), \n",
" либо получателем (`receiver`).\n",
" \"\"\",\n",
" tags=[\"Profile\", \"Delivery\"],\n",
")\n",
"async def profile_delivery(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" \"\"\"\n",
" Рендерит HTML-страницу с доставками текущего пользователя.\n",
"\n",
" Запрашивает все документы из коллекции `delivery`, где пользователь \n",
" участвует как отправитель или получатель.\n",
"\n",
" Args:\n",
" request: FastAPI Request (для шаблонизатора)\n",
" user: Текущий авторизованный пользователь (из Security)\n",
"\n",
" Returns:\n",
" HTMLResponse с шаблоном profile/delivery.html\n",
" \"\"\"\n",
" # Получаем все доставки пользователя (отправитель ИЛИ получатель)\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}}, # why not work\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list()\n",
" competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"exhibitions\": exhibitions,\n",
" \"competitions\": competitions,\n",
" \"data\": data,\n",
" \"now\": datetime.now(),\n",
" \"page_title\": \"Доставка\",\n",
" \"active_section\": \"delivery\"\n",
" }\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "4f58b246",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/exhibition/send/invitations\")\n",
"async def api_exhibition_send_invitations(\n",
" background_tasks: BackgroundTasks,\n",
" exhibition_id: Optional[str] = Form(...),\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" background_tasks.add_task(exhibition_send_invitations, exhibition_id)\n",
" logging.info(f\"send exh inv {exhibition_id}\")\n",
" return {\"ok\": True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b5d81a2",
"metadata": {},
"outputs": [],
"source": [
"@app.post(\"/api/exhibition/post/select\")\n",
"async def api_exhibition_post_select(\n",
" post_id: Optional[str] = Form(...),\n",
" exhibition_id: Optional[str] = Form(...),\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" await exhibition_collection.update_one(\n",
" {\n",
" \"_id\": ObjectId(exhibition_id),\n",
" \"user._id\": user.get(\"_id\")\n",
" },\n",
" [\n",
" {\n",
" \"$set\": {\n",
" \"posts\": {\n",
" \"$cond\": [\n",
" {\"$in\": [post_id, {\"$ifNull\": [\"$posts\", []]}]},\n",
" {\"$setDifference\": [\"$posts\", [post_id]]},\n",
" {\"$setUnion\": [\"$posts\", [post_id]]}\n",
" ]\n",
" }\n",
" }\n",
" }\n",
" ]\n",
" )\n",
" return {\"ok\": True}\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "5ca78d9c",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/partner/exhibition/{exhibition_id}/catalog\", response_class=HTMLResponse)\n",
"async def profile_partner_exhibitions_catalog(\n",
" request: Request,\n",
" exhibition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
" cursor = posts_collection.aggregate([\n",
" {\n",
" \"$match\": {\n",
" \"city\": {\"$nin\": [None, \"\"]}\n",
" }\n",
" },\n",
" { '$group': { '_id': \"$city\", 'count': { '$sum': 1 } } },\n",
" { '$sort': { '_id': 1 } }\n",
" ])\n",
"\n",
" living_places = [doc async for doc in cursor]\n",
" cursor = posts_collection.aggregate([\n",
" {\n",
" \"$match\": {\n",
" \"genre\": {\"$nin\": [None, \"\"]}\n",
" }\n",
" },\n",
" {\n",
" \"$project\": {\n",
" \"genres\": {\"$split\": [\"$genre\", \",\"]}\n",
" }\n",
" },\n",
" {\"$unwind\": \"$genres\"},\n",
" {\n",
" \"$match\": {\n",
" \"genres\": {\"$nin\": [\"\", None]}\n",
" }\n",
" },\n",
" {\n",
" \"$group\": {\n",
" \"_id\": \"$genres\",\n",
" \"count\": {\"$sum\": 1}\n",
" }\n",
" },\n",
" {\"$sort\": {\"_id\": 1}}\n",
" ])\n",
"\n",
" genres = [{'_id': doc[\"_id\"].strip(), 'count': doc[\"count\"]} async for doc in cursor]\n",
" users = await users_collection.find(\n",
" {\"roles\": {\"$in\": [\"partner\", \"galerist\"]}},\n",
" {\"username\": 1, \"_id\": 0}\n",
" ).to_list()\n",
"\n",
" usernames = [u[\"username\"] for u in users]\n",
" pipeline = [\n",
" {\"$match\": {\"user.roles\": {\"$in\": [\"partner\", \"galerist\"]}}},\n",
" {\"$group\": {\"_id\": \"$user.username\", \"count\": {\"$sum\": 1}}},\n",
" ]\n",
" cursor = posts_collection.aggregate(pipeline)\n",
" post_counts = await cursor.to_list(length=None)\n",
"\n",
" counts_by_username = {d[\"_id\"]: d[\"count\"] for d in post_counts}\n",
"\n",
" galerists = [{\"_id\": uname, \"count\": counts_by_username.get(uname, '')} for uname in usernames]\n",
" posts = [doc async for doc in posts_collection.find().sort('_id', -1).limit(32)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibition catalog.html\", context={\"now\": datetime.now(), \"user\": user, \"galerists\": galerists, \"genres\": genres, \"living_places\": living_places, \"exhibition\": exhibition, \"posts\": posts}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "b29511be",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/profile/partner/exhibition/{exhibition_id}\", response_class=HTMLResponse)\n",
"async def profile_partner_exhibitions(\n",
" request: Request,\n",
" exhibition_id: str,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
" obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]\n",
" posts = await posts_collection.find({\"_id\": {\"$in\": obj_ids}}).to_list() if obj_ids else []\n",
" d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()\n",
" for post in posts:\n",
" for doc in d_posts:\n",
" if str(post.get('_id')) == doc.get('post_id') and doc.get('receiver') and doc.get('receiver').get('_id') == str(user.get('_id')):\n",
" post.update({'status':doc.get('status'), 'receiver': doc.get('receiver'), 'd_id':doc.get('_id')})\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibition.html\", context={\"now\": datetime.now(), \"user\": user, \"exhibition\": exhibition, \"posts\": posts}\n",
" )\n",
"\n",
"\n",
"@app.get(\"/profile/partner/exhibitions\", response_class=HTMLResponse)\n",
"async def profile_exhibitions(\n",
" request: Request,\n",
" user: Dict = Security(get_current_user)\n",
"):\n",
" data = await exhibition_collection.find({'user._id': user.get('_id')}).sort('_id', -1).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibitions.html\", context={\"now\": datetime.now(), \"user\": user, \"data\": data}\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "8810213f",
"metadata": {},
"source": [
"## Run"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23ac9374",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import os\n",
"\n",
"IGNORE = ['build.ipynb', 'data.ipynb']\n",
"\n",
"for _filename in [i for i in os.listdir() if 'ipynb' in i and i not in IGNORE]:\n",
" filename_without_extension = os.path.splitext(os.path.basename(_filename))[0]\n",
"\n",
" with open(f'{filename_without_extension}.ipynb', 'r') as f:\n",
" data = f.read()\n",
"\n",
" main = json.loads(data)\n",
" app = []\n",
" for cell in main['cells']:\n",
" if '## Run' in cell['source']:\n",
" break\n",
" if '# ignore\\n' in cell['source']:\n",
" continue\n",
" app.append(''.join(cell['source']))\n",
"\n",
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
" f.write('\\n\\n'.join(app))\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "c7dc7c3e",
"metadata": {
"vscode": {
"languageId": "shellscript"
}
},
"outputs": [],
"source": [
"!uvicorn main:app --host 0.0.0.0 --port 8005 --reload"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "abf4c2df",
"metadata": {},
"outputs": [],
"source": [
"import uvicorn\n",
"uvicorn.run('main:app', host='0.0.0.0', port=8005, reload=True)\n"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "62a866f8",
"metadata": {},
"source": [
"## Test"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a6f67f24",
"metadata": {
"vscode": {
"languageId": "shellscript"
}
},
"outputs": [],
"source": [
"!pip freeze > req.txt"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d49d0bd",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.executable"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a9444d9",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.executable\n",
"!{sys.executable} -m pip install -r requirements-api.txt"
],
"display_name": ".venv"
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}