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