{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "4563b7d7", "metadata": {}, "outputs": [], "source": [ "# ignore\n", "import os\n", "\n", "def load_env(file_path):\n", " \"\"\"Загружает переменные окружения из файла .env с поддержкой булевых значений.\"\"\"\n", " with open(file_path) as f:\n", " for line in f:\n", " line = line.strip()\n", " if not line or line.startswith('#'):\n", " continue\n", " \n", " try:\n", " key, value = line.split('=', 1)\n", " key = key.strip()\n", " value = value.strip('\"\\' ')\n", " # Преобразование строковых значений в булевые\n", " if value.lower() in ['true', '1', 'yes']:\n", " value = True\n", " elif value.lower() in ['false', '0', 'no']:\n", " value = False\n", " \n", " # Проверка, что ключ не пустой\n", " if key:\n", " os.environ[key] = str(value) # Храним как строку\n", " except ValueError:\n", " print(f\"Ошибка: неверный формат в строке '{line}'\")\n", "\n", "try:\n", " load_env('env')\n", "except:\n", " load_env('env_example')" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "b66cc5eb", "metadata": {}, "outputs": [], "source": [ "from config import *\n", "from db import *\n", "from typing import Dict, Any, List, Optional, Callable, Union\n", "import email_task as email_task\n", "import vk as vk\n", "import tg as tg\n", "from utils import *\n", "from models import NotifyItem\n", "from auth import token_create\n", "import asyncio\n" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "019d30b5", "metadata": {}, "outputs": [], "source": [ "async def notify_in_profile(item: NotifyItem):\n", " result = await notify_collection.update_one(\n", " {\n", " \"user_id\": item.user_id,\n", " \"type\": item.type_item,\n", " \"title\": item.title,\n", " \"link\": item.link,\n", " \"text\": item.text,\n", " },\n", " {\n", " \"$set\": {\n", " \"user_id\": item.user_id,\n", " \"type\": item.type_item,\n", " \"title\": item.title,\n", " \"text\": item.text,\n", " \"link\": item.link,\n", " \"read\": True,\n", " \"prod\": PROD,\n", " }\n", " },\n", " upsert=True,\n", " )\n", "\n", " if result.upserted_id is None:\n", " return result # Existing notification was updated; no email\n", "\n", " user = await users_collection.find_one(\n", " {\"_id\": ObjectId(str(item.user_id))},\n", " {\"email\": 1},\n", " )\n", "\n", " if not user:\n", " return result\n", "\n", " token = await token_create(\n", " str(user[\"_id\"]),\n", " url=\"/profile\",\n", " )\n", " login_link = f\"{FULL_DOMAIN}/login/{token}\"\n", "\n", " if PROD:\n", " email_task.notify.delay(\n", " user[\"email\"],\n", " {\n", " \"text\": item.text,\n", " \"list\": [[]],\n", " \"primary\": {\n", " \"href\": login_link,\n", " \"text\": \"Войти и посмотреть\",\n", " },\n", " \"title\": item.title,\n", " \"login_link\": login_link,\n", " },\n", " \"Уведомление\",\n", " \"main.html\",\n", " )\n", "\n", " return result\n" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "f26f18e9", "metadata": {}, "outputs": [], "source": [ "async def email_new_post(new_post: Dict):\n", " async for user in users_collection.find({'roles':'partner'}):\n", " login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/paintings/{new_post['_id']}')}'\n", " if PROD:\n", " email_task.notify.delay(user['email'], {\n", " 'text':'На сервисе опубликована новая картина',\n", " 'list':[[\n", " {'label':'Название','text':new_post['title'], 'img':f\"{FULL_DOMAIN}/b/posts/{new_post['medias_original'][0]}\"},\n", " {'label':'Звание художника','text':new_post['grade']},\n", " {'label':'Город','text':new_post['city']},\n", " {'label':'Размер','text':new_post['size']},\n", " {'label':'Материал','text':new_post['material']},\n", " {'label':'Жанр','text':new_post['genre']},\n", " {'label':'Цена','text':new_post['price']},\n", " ]],\n", " 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n", " 'title': f\"от художника {new_post['author']}\",\n", " 'login_link': login_link,\n", " },\n", " 'Новая картина', 'main.html')\n" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "cbff5107", "metadata": {}, "outputs": [], "source": [ "async def update_social_post(post):\n", " pass" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "e09b70e2", "metadata": {}, "outputs": [], "source": [ "async def publish_social_post(post):\n", " query = {\n", " 'active': True,\n", " \"$or\": [{'service': True}, {'user_id': post['user']['_id']}]\n", " }\n", " post_ref = {'_id': post['_id'], 'db': 'posts', 'col': 'posts'}\n", " medias = post.get(\"medias_original\", [])\n", "\n", " platforms = [\n", " ('tg', tg, lambda s: [\n", " f\"{FULL_PATH_DOMAIN}/a/{post['user']['_id']}\" if should_sign_avatar(post, s) else None,\n", " *[f\"{FULL_PATH_DOMAIN}/b/posts/{m}\" for m in medias]\n", " ]),\n", " ('vk', vk, lambda s: [\n", " post['user']['_id'] if should_sign_avatar(post, s) else None,\n", " *medias\n", " ])\n", " ]\n", "\n", " for platform_type, client, get_attachments in platforms:\n", " if post.get(platform_type) != []:\n", " continue\n", " socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n", " socials = {item[\"id\"]: item for item in socials}.values()\n", " for social in socials:\n", " text = parse_posts_text(post, platform_type, social.get('service', False))\n", " sign_avatar = should_sign_avatar(post, social)\n", " attachments = [a for a in get_attachments(social) if a is not None]\n", " \n", " client.notify.delay(post_ref, text, attachments, bson_clear(social), sign_avatar)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "846846ca", "metadata": {}, "outputs": [], "source": [ "async def user_social_competition(competition):\n", " async for user in users_collection.find({'roles':'admin'}):\n", " # email_task.notify.delay\n", " if PROD:\n", " pass\n", " else:\n", " print(competition)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "829dfa3d", "metadata": {}, "outputs": [], "source": [ "class SocialContentHandler:\n", " \"\"\"Base class for content type specific handlers\"\"\"\n", " \n", " def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n", " \"\"\"Return reference for the content item\"\"\"\n", " raise NotImplementedError\n", " \n", " def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n", " \"\"\"Return list of media file_ids\"\"\"\n", " raise NotImplementedError\n", " \n", " def get_text(self, item: Dict[str, Any]) -> str:\n", " \"\"\"Parse text for the content\"\"\"\n", " raise NotImplementedError\n", "\n", "\n", "class CompetitionHandler(SocialContentHandler):\n", " def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n", " return {'_id': item['_id'], 'db': 'competitions', 'col': 'competitions'}\n", " \n", " def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n", " return [item.get(\"poster_file_id\")]\n", " \n", " def get_text(self, item: Dict[str, Any]) -> str:\n", " return parse_text_competition(item)\n", "\n", "\n", "class PostHandler(SocialContentHandler):\n", " def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n", " return {'_id': item['_id'], 'db': 'posts', 'col': 'posts'}\n", " \n", " def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n", " # Adjust according to your Post schema\n", " return item.get(\"media_file_ids\", []) or [item.get(\"main_image\")]\n", " \n", " def get_text(self, item: Dict[str, Any]) -> str:\n", " return item.get(\"text\", \"\")\n", "\n", "\n", "class NewsHandler(SocialContentHandler):\n", " def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n", " return {'_id': item['_id'], 'db': 'news', 'col': 'news'}\n", " \n", " def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n", " return item.get(\"media_ids\", [])\n", " \n", " def get_text(self, item: Dict[str, Any]) -> str:\n", " return item.get(\"title\", \"\") + \"\\n\\n\" + item.get(\"content\", \"\")\n", "\n", "\n", "# Registry for handlers\n", "CONTENT_HANDLERS: Dict[str, SocialContentHandler] = {\n", " \"competition\": CompetitionHandler(),\n", " \"post\": PostHandler(),\n", " \"news\": NewsHandler(),\n", " # Add more: \"user\": UserProfileHandler(), etc.\n", "}\n", "\n", "\n", "class SocialPlatform:\n", " \"\"\"Platform configuration\"\"\"\n", " def __init__(\n", " self,\n", " type_name: str,\n", " client: Any,\n", " get_attachments: Callable[[Dict], List[str]],\n", " sign_avatar_func: Optional[Callable] = None\n", " ):\n", " self.type = type_name\n", " self.client = client\n", " self.get_attachments = get_attachments\n", " self.sign_avatar_func = sign_avatar_func or should_sign_avatar\n", "\n", "\n", "# Platform registry - easy to extend\n", "PLATFORMS: Dict[str, SocialPlatform] = {\n", " \"tg\": SocialPlatform(\n", " type_name=\"tg\",\n", " client=tg,\n", " get_attachments=lambda social, medias: [\n", " f\"{FULL_PATH_DOMAIN}/a/{social.get('user_id')}\" if should_sign_avatar(social) else None,\n", " *[f\"{FULL_PATH_DOMAIN}/b/competition/{m}\" for m in medias if m] # generalize path\n", " ]\n", " ),\n", " \"vk\": SocialPlatform(\n", " type_name=\"vk\",\n", " client=vk,\n", " get_attachments=lambda social, medias: [\n", " social.get('user_id') if should_sign_avatar(social) else None,\n", " *medias\n", " ]\n", " ),\n", " # Add more platforms easily:\n", " # \"ok\": SocialPlatform(...),\n", " # \"telegram_channel\": ...\n", "}\n", "\n", "\n", "async def get_socials_for_content(\n", " content_type: str,\n", " item: Dict[str, Any],\n", " service_social: bool = False,\n", " user_social: bool = False,\n", " users_social: bool = False\n", ") -> List[Dict]:\n", " \"\"\"Get relevant social accounts based on rules\"\"\"\n", " query: Dict = {'active': True, \"$or\": []}\n", " \n", " if service_social:\n", " query[\"$or\"].append({'service': True})\n", " if user_social:\n", " query[\"$or\"].append({'user_id': item.get('user', {}).get('_id')})\n", " if users_social:\n", " query[\"$or\"].extend([\n", " {\"competition\": True},\n", " {\"competition\": {\"$exists\": False}}\n", " ])\n", " \n", " # Filter by platform types that exist in registry\n", " platform_types = list(PLATFORMS.keys())\n", " cursor = social_collection.find({**query, 'type': {'$in': platform_types}})\n", " socials = [doc async for doc in cursor]\n", " \n", " # Deduplicate by id\n", " return list({item[\"id\"]: item for item in socials}.values())\n", "\n", "\n", "async def send_to_social(\n", " content_type: str,\n", " item: Dict[str, Any],\n", " social: Dict[str, Any],\n", " platform: SocialPlatform\n", ") -> bool:\n", " \"\"\"Send one item to one social account with spam protection\"\"\"\n", " handler = CONTENT_HANDLERS.get(content_type)\n", " if not handler:\n", " print(f\"Unknown content type: {content_type}\")\n", " return False\n", "\n", " redis_key = f\"nospamsends:{content_type}:{item['_id']}:{social['_id']}\"\n", " \n", " already_sent = await redis_client.set(\n", " redis_key,\n", " \"1\",\n", " ex=8 * 60 * 60, # 8 hours\n", " nx=True\n", " )\n", " \n", " if not already_sent:\n", " return False # already sent recently\n", "\n", " post_ref = handler.get_post_ref(item)\n", " medias = handler.get_medias(item)\n", " text = handler.get_text(item)\n", " \n", " sign_avatar = should_sign_avatar(item, social) if hasattr(handler, 'should_sign') else should_sign_avatar(item, social)\n", " \n", " attachments = [a for a in platform.get_attachments(social, medias) if a is not None]\n", " \n", " try:\n", " platform.client.notify.delay(\n", " post_ref,\n", " text,\n", " attachments,\n", " bson_clear(social),\n", " sign_avatar\n", " )\n", " print(f\"Queued {content_type} {item['_id']} to {platform.type} social {social['_id']}\")\n", " return True\n", " except Exception as e:\n", " print(f\"Failed to queue to {platform.type}: {e}\")\n", " return False\n", "\n", "\n", "async def service_social_publish(\n", " content_type: str,\n", " item: Dict[str, Any],\n", " service_social: bool = False,\n", " user_social: bool = False,\n", " users_social: bool = False\n", "):\n", " \"\"\"\n", " Generic social publishing service.\n", " \n", " Usage:\n", " await service_social_publish(\"competition\", competition_dict)\n", " await service_social_publish(\"post\", post_dict)\n", " \"\"\"\n", " if content_type not in CONTENT_HANDLERS:\n", " raise ValueError(f\"Unsupported content type: {content_type}\")\n", " \n", " socials = await get_socials_for_content(\n", " content_type, item, service_social, user_social, users_social\n", " )\n", " \n", " tasks = []\n", " for social in socials:\n", " platform_key = social.get(\"type\")\n", " platform = PLATFORMS.get(platform_key)\n", " if not platform:\n", " continue\n", " \n", " tasks.append(send_to_social(content_type, item, social, platform))\n", " \n", " if tasks:\n", " await asyncio.gather(*tasks, return_exceptions=True)\n", "\n", "\n", "# # Convenience wrappers\n", "# async def service_social_competition(competition: Dict, **kwargs):\n", "# return await service_social_publish(\"competition\", competition, **kwargs)\n", "\n", "\n", "# async def service_social_post(post: Dict, **kwargs):\n", "# return await service_social_publish(\"post\", post, **kwargs)\n", "\n", "\n", "# async def service_social_news(news: Dict, **kwargs):\n", "# return await service_social_publish(\"news\", news, **kwargs)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "6b68a608", "metadata": {}, "outputs": [], "source": [ "async def service_social_exhibition(exhibition):\n", " query = { 'active': True, \"$or\": []}\n", " query[\"$or\"].append({'service': True})\n", " query[\"$or\"].append({'user_id': exhibition['user']['_id']})\n", "\n", " post_ref = {'_id': str(exhibition['_id']), 'db': 'exhibition', 'col': 'exhibition'}\n", " medias = [exhibition.get(\"poster_orig_id\")]\n", "\n", " platforms = [\n", " ('tg', tg, lambda s: [\n", " # f\"{FULL_PATH_DOMAIN}/a/{exhibition['user']['_id']}\" if should_sign_avatar(exhibition, s) else None,\n", " *[f\"{FULL_PATH_DOMAIN}/b/exhibition/{m}\" for m in medias]\n", " ]),\n", " ('vk', vk, lambda s: [\n", " # exhibition['user']['_id'] if should_sign_avatar(exhibition, s) else None,\n", " *medias\n", " ])\n", " ]\n", "\n", " for platform_type, client, get_attachments in platforms:\n", " if exhibition.get(platform_type) != []:\n", " continue\n", " socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n", " socials = {item[\"id\"]: item for item in socials}.values()\n", " for social in socials:\n", " text = parse_text_exhibition(exhibition)\n", " sign_avatar = should_sign_avatar(exhibition, social)\n", " attachments = [a for a in get_attachments(social) if a is not None]\n", " redis_key = f\"nospamsends:{platform_type}:{exhibition['_id']}:{social['_id']}\"\n", " already_sent = await redis_client.set(\n", " redis_key, \n", " 1, \n", " ex=8*60*60,\n", " nx=True\n", " )\n", " print(f\"nospamsends:{platform_type}:{exhibition['_id']}:{social['_id']}\")\n", " if already_sent:\n", " print(social)\n", " client.notify.delay(post_ref, text, attachments, bson_clear(social), False)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "e19b21ec", "metadata": {}, "outputs": [], "source": [ "async def service_social_competition(competition, serviceSocial, userSocial, usersSocial):\n", " query = { 'active': True, \"$or\": []}\n", " if serviceSocial:\n", " query[\"$or\"].append({'service': True})\n", "\n", " if userSocial:\n", " query[\"$or\"].append({'user_id': competition['user']['_id']})\n", "\n", " if usersSocial:\n", " query[\"$or\"].append({\"competition\": True})\n", " query[\"$or\"].append({\"competition\": {\"$exists\": False}})\n", " post_ref = {'_id': str(competition['_id']), 'db': 'competitions', 'col': 'competitions'}\n", " medias = [competition.get(\"poster_file_id\")]\n", "\n", " platforms = [\n", " ('tg', tg, lambda s: [\n", " # f\"{FULL_PATH_DOMAIN}/a/{competition['user']['_id']}\" if should_sign_avatar(competition, s) else None,\n", " *[f\"{FULL_PATH_DOMAIN}/b/competition/{m}\" for m in medias]\n", " ]),\n", " ('vk', vk, lambda s: [\n", " # competition['user']['_id'] if should_sign_avatar(competition, s) else None,\n", " *medias\n", " ])\n", " ]\n", "\n", " for platform_type, client, get_attachments in platforms:\n", " \n", " socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n", " socials = {item[\"id\"]: item for item in socials}.values()\n", " for social in socials:\n", " text = parse_text_competition(competition)\n", " sign_avatar = should_sign_avatar(competition, social)\n", " attachments = [a for a in get_attachments(social) if a is not None]\n", " redis_key = f\"nospamsends:{platform_type}:{competition['_id']}:{social['_id']}\"\n", " already_sent = await redis_client.set(\n", " redis_key, \n", " 1, \n", " ex=8*60*60,\n", " nx=True\n", " )\n", " print(f\"nospamsends:{platform_type}:{competition['_id']}:{social['_id']}\")\n", " if already_sent:\n", " print(social)\n", " client.notify.delay(post_ref, text, attachments, bson_clear(social), False)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "031c0d4a", "metadata": {}, "outputs": [], "source": [ "async def email_new_user(new_user):\n", " async for user in users_collection.find({'roles':'admin'}):\n", " login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/artists/{new_user['_id']}')}'\n", " role = {'user':'Художник','galerist':'Галерея',\"reporter\":'Репортер'}[new_user['roles'][0]]\n", " if PROD:\n", " email_task.notify.delay(user['email'], {\n", " 'list':[[\n", " {'label':'ФИО','text': new_user['female_name_lastname'], 'img':f\"{FULL_DOMAIN}/a/{new_user['_id']}\"},\n", " {'label':'Звание','text': new_user['grade']} if new_user['roles'][0] != 'galerist' else {},\n", " {'label':'Город проживания','text': new_user['living_place']}\n", " ]],\n", " 'text': f'',\n", " 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n", " 'title': f\"{role} {new_user['username']}\",\n", " 'login_link': login_link\n", " },\n", " 'Новый пользователь', 'main.html')" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "1d03d7e1", "metadata": {}, "outputs": [], "source": [ "async def invite_users_to_competitions(doc):\n", " new_notify = []\n", "\n", " query = {\n", " \"roles\": {\"$in\": [\"user\"]},\n", " \"$or\": [\n", " {\"email_subscribed\": True},\n", " {\"email_subscribed\": {\"$exists\": False}}\n", " ]\n", " }\n", "\n", " async for user in users_collection.find(query):\n", " if not user.get(\"email\"):\n", " continue\n", "\n", " redis_key = f\"nospam:{user['email']}:{doc['_id']}\"\n", "\n", " already_sent = await redis_client.set(\n", " redis_key, \n", " 1, \n", " ex=24*60*60,\n", " nx=True\n", " )\n", "\n", " if already_sent:\n", " new_notify.append(\n", " notify_in_profile(NotifyItem(\n", " user_id=str(user['_id']),\n", " type_item='competition',\n", " title='Приглашение на конкурс',\n", " link=f'/profile/competition/{doc.get(\"id\") or doc.get(\"_id\")}',\n", " text=f'«{doc['title']}» Организатор {doc['user']['username']}'\n", " ))\n", " )\n", " if PROD:\n", " email_task.notify.delay(user['email'], {'competition': bson_clear(doc), 'login_link': f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/competition/{doc.get(\"id\") or doc.get(\"_id\")}')}'}, 'Конкурс', 'competition.html')\n", " await asyncio.gather(*new_notify)" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "3e22dab5", "metadata": {}, "outputs": [], "source": [ "async def accept_competition_req(competition, user_id):\n", " user = await users_collection.find_one({'_id': ObjectId(user_id)},{'email':1})\n", " login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/profile/invitation')}'\n", " if PROD:\n", " email_task.notify.delay(user['email'], {\n", " 'text':f'на конкурсе {competition['title']}',\n", " 'list':[[ ]],\n", " 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n", " 'title': f\"Вы прошли отбор\",\n", " 'login_link': login_link,\n", " },\n", " 'Приглашение', 'main.html')\n", " await notify_in_profile(NotifyItem(user_id=user_id, type_item='invation', title='Приглашение', link='/profile/invitation', text=f'Вы прошли отбор на конкурсе {competition['title']}'))\n" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "b25adab4", "metadata": {}, "outputs": [], "source": [ "async def get_sender(post_id):\n", " delivery_cursor = delivery_collection.find(\n", " {\n", " 'post_id': post_id,\"prod\": PROD,\n", " 'status': { '$nin': [\"Ожидает отправки\", \"Ожидает принятия условий\", \"Третье\"] }\n", " },\n", " {'receiver': 1}\n", " ).sort('_id', 1) # 1 = ascending, -1 = descending (newest first)\n", "\n", " delivery = await delivery_cursor.limit(1).to_list(length=1)\n", " delivery = delivery[0] if delivery else {}\n", " return delivery.get('receiver')" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "3c8e1473", "metadata": {}, "outputs": [], "source": [ "from typing import List\n", "from bson import ObjectId\n", "\n", "async def accept_invitations(invitations_id: str, posts_ids: List[str]):\n", " invitation = await invitations_collection.find_one_and_update(\n", " {\"_id\": ObjectId(invitations_id)},\n", " {\"$set\": {\"accept\": True}},\n", " return_document=True,\n", " )\n", "\n", " invitation_posts = invitation.get('posts', []) or []\n", " invitation_post_ids = [str(doc['_id']) for doc in invitation_posts]\n", "\n", " selected_set = set(str(x) for x in posts_ids)\n", " selected_ids = [pid for pid in invitation_post_ids if pid in selected_set]\n", " other_ids = [pid for pid in invitation_post_ids if pid not in selected_set]\n", "\n", " if selected_ids:\n", " await delivery_collection.update_many(\n", " {'post_id': {'$in': selected_ids}},\n", " {'$set': {'status': 'Ожидает отправки'}}\n", " )\n", "\n", " if other_ids:\n", " await delivery_collection.update_many(\n", " {'post_id': {'$in': other_ids}},\n", " {'$set': {'status': 'Приглашение отклонено'}} # как вы написали\n", " )\n", " if (exhibition:=invitation.get('exhibition',{})) and (exhibition_id:=invitation.get('exhibition_id')):\n", " link = f'/profile/delivery/exhibition/{exhibition_id}'\n", " else:\n", " link = f'/profile/delivery'\n", " await notify_in_profile(NotifyItem(\n", " user_id=exhibition.get(\"user\",{}).get('_id'),\n", " type_item='c',\n", " title=f'Выставка \"{exhibition.get(\"title\")}\"',\n", " link=link,\n", " text=f'Художник принял условия'\n", " ))" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "dc239a3e", "metadata": {}, "outputs": [], "source": [ "async def exhibition_send_invitations(exhibition_id):\n", " exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n", " exhibition_owner = exhibition.get('user')\n", " obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]\n", " posts = await posts_collection.find({\"_id\": {\"$in\": obj_ids}, 'status': 'В каталоге'}).to_list() if obj_ids else []\n", " new_posts = {}\n", " all_posts = {}\n", " for post in posts:\n", " post_id = str(post.get('_id'))\n", " user = display_user(post.get('user'))\n", " doc = await delivery_collection.update_one(\n", " {\n", " \"exhibition_id\": str(exhibition.get('_id')),\"post_id\": post_id, \"status\": \"Ожидает принятия условий\", \"type\": \"exhibition\",\n", " },{\n", " \"$set\":{\n", " \"sender\": user,\n", " \"user\": user,\n", " \"receiver\": display_user(exhibition_owner),\n", " \"exhibition_id\": str(exhibition.get('_id')),\n", " \"address\": exhibition.get('address'),\n", " \"post_id\": post_id, \n", " \"prod\": PROD,\n", " \"img\": post.get('medias',[0])[0],\n", " \"title\": post.get('title'),\n", " \"status\": \"Ожидает принятия условий\",\n", " \"type\": \"exhibition\",\n", " \"updated_at\": datetime.utcnow()\n", " }\n", " }, upsert=True)\n", " if doc.upserted_id:\n", " if (user_id:=user.get('_id')) in new_posts:\n", " new_posts[user_id].append(post)\n", " else:\n", " new_posts.update({user_id: [post]})\n", " if (user_id:=user.get('_id')) in all_posts:\n", " all_posts[user_id].append(post)\n", " else:\n", " all_posts.update({user_id: [post]})\n", " print(all_posts)\n", " if all_posts:\n", " for user_id, posts in all_posts.items():\n", " doc = await invitations_collection.update_one(\n", " {'exhibition_id': str(exhibition.get('_id')),'user_id': user_id,'type': 'Выставка'},\n", " \n", " {\"$set\":{'exhibition': bson_clear(exhibition), \n", " 'exhibition_id': str(exhibition.get('_id')),\n", " 'accept': False, \n", " 'user':display_user(exhibition_owner), \n", " 'user_id': user_id, \n", " 'posts': posts,\n", " 'prod': PROD, \n", " 'type': 'Выставка', \n", " 'item': exhibition.get('title')}},\n", " upsert=True\n", " )\n", " if new_posts:\n", " for user_id, posts in new_posts.items():\n", " await notify_in_profile(NotifyItem(\n", " user_id=user_id,\n", " type_item='invitation',\n", " title='Приглашение',\n", " link=f'/profile/invitation',\n", " text=f'Приглашаем на выставку \"{exhibition.get(\"title\")}\"'\n", " ))\n", " \n", " user = posts[0].get('user')\n", " token = await token_create(\n", " str(user['_id']), \n", " url=f'/profile/invitation#main'\n", " )\n", " login_link = f'{FULL_DOMAIN}/login/{token}'\n", " email_data = {\n", " 'text': 'Приглашаем на выставку со следующими картинами: ',\n", " 'list': [\n", " [\n", " {'label': 'Название', 'text': item.get('title', ''), 'img':f\"{FULL_DOMAIN}/b/posts/{item['medias_original'][0]}\"},\n", " ]\n", " for item in posts\n", " ],\n", " 'primary': {\n", " 'href': login_link,\n", " 'text': 'Войти и посмотреть'\n", " },\n", " 'title': exhibition.get('title', 'Выставка'),\n", " 'login_link': login_link,\n", " }\n", " if PROD:\n", " email_task.notify.delay(\n", " user['email'],\n", " email_data,\n", " f'Приглашение на выставку {exhibition.get('title')}',\n", " 'main.html'\n", " )\n", " \n" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "5db0ae46", "metadata": {}, "outputs": [], "source": [ "async def update_competition_req(competition, user, posts):\n", " competition_id = competition.get('_id')\n", "\n", " if not user or not posts or not competition_id:\n", " return\n", "\n", " posts_sorted = sorted(posts, key=lambda p: p.get('status') != 'Проходит')\n", "\n", " # Генерация login link\n", " token = await token_create(\n", " str(user['_id']), \n", " url=f'/profile/invitation#main'\n", " )\n", " login_link = f'{FULL_DOMAIN}/login/{token}'\n", "\n", " if True:\n", " competition_owner = competition.get('user', {})\n", "\n", " # === Создание доставок ===\n", " \n", " post_ids_to_mark = [] # для последующего обновления delivery: True\n", " post_ids_to_remove_delivery = []\n", " for post in posts:\n", " post_id = str(post.get('_id'))\n", " if post.get('status') == 'Проходит':\n", " sender = await get_sender(post_id) or display_user(user)\n", " delivery_collection.update_one({\n", " \"competition_id\": competition_id,\"post_id\": post_id, \"status\": \"Ожидает отправки\",\n", " \"sender._id\": sender.get('_id'), \"receiver._id\": competition_owner.get('_id')\n", " },{\n", " \"$set\":{\n", " \"sender\": sender,\n", " \"user\": display_user(user),\n", " \"receiver\": display_user(competition_owner),\n", " \"competition_id\": competition_id,\n", " \"address\": competition.get('address'),\n", " \"post_id\": post_id,\n", " \"prod\": PROD,\n", " \"img\": post.get('medias',[0])[0],\n", " \"title\": post.get('title'),\n", " \"status\": \"Ожидает отправки\",\n", " \"type\": \"competition_reward\",\n", " \"updated_at\": datetime.utcnow()\n", " }\n", " }, upsert=True)\n", " post_ids_to_mark.append(post_id)\n", " elif post.get('status') != 'Проходит':\n", " post_ids_to_remove_delivery.append(post_id)\n", "\n", "\n", " if post_ids_to_remove_delivery:\n", " await delivery_collection.delete_many({\n", " \"competition_id\": competition_id,\n", " \"status\": \"Ожидает отправки\",\n", " \"post_id\": {\"$in\": post_ids_to_remove_delivery}\n", " })\n", "\n", " await notify_in_profile(NotifyItem(\n", " user_id=str(user['_id']),\n", " type_item='invitation',\n", " title='Приглашение',\n", " link=f'/profile/invitation',\n", " text=f'Вы прошли отбор на конкурсе \"{competition.get(\"title\")}\"'\n", " ))\n", " all_allowed = all(\n", " post.get('status') in ('Проходит', 'На рассмотрении')\n", " for post in posts\n", " )\n", " has_approved = any(p.get('status') == 'Проходит' for p in posts)\n", "\n", " # Email-уведомление\n", " if user.get('email') and all_allowed and has_approved:\n", " await invitations_collection.update_one({\n", " \"user_id\": str(user['_id']),\n", " \"item\": competition.get(\"title\"),\n", " \"title\": \"Вы прошли отбор\",\n", " \"type_\": competition.get(\"type_\"),\n", " \"user\": display_user(competition_owner),\n", " \"prod\": PROD,\n", " \"type\": \"Конкурс\"\n", " },{'$set':{\n", " \"user_id\": str(user['_id']),\n", " \"item\": competition.get(\"title\"),\n", " \"title\": \"Вы прошли отбор\",\n", " \"competition\": competition,\n", " \"type_\": competition.get(\"type_\"),\n", " \"user\": display_user(competition_owner),\n", " \"prod\": PROD,\n", " \"type\": \"Конкурс\"\n", " }},upsert=True)\n", " if PROD and user.get('email') and all_allowed and has_approved:\n", " email_data = {\n", " 'text': 'Статус заявки обновлен',\n", " 'list': [\n", " [\n", " {'label': 'Название', 'text': item.get('title', ''), 'img':f\"{FULL_DOMAIN}/b/posts/{item['medias_original'][0]}\"},\n", " {'label': 'Статус', 'text': item.get('status', '')}\n", " ]\n", " for item in posts_sorted\n", " ],\n", " 'primary': {\n", " 'href': login_link,\n", " 'text': 'Войти и посмотреть'\n", " },\n", " 'title': competition.get('title', 'Конкурс'),\n", " 'login_link': login_link,\n", " }\n", "\n", " email_task.notify.delay(\n", " user['email'],\n", " email_data,\n", " 'Обновление статуса заявки на конкурс',\n", " 'main.html'\n", " )" ], "display_name": ".venv" }, { "cell_type": "code", "execution_count": null, "id": "8ca4a900", "metadata": {}, "outputs": [], "source": [ "async def new_competiton_req(request):\n", " competition = await competitions_col.find_one({'_id':ObjectId(request['competition_id'])})\n", " user = competition['user']\n", " if competition.get('type_') == 'artist':\n", " link = f'/profile/partner/competition/{request['competition_id']}/{request['user']['_id']}'\n", " else:\n", " link = f'/profile/partner/competition/{request['competition_id']}'\n", " login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=link)}'\n", " print(user)\n", " await notify_in_profile(NotifyItem(\n", " user_id=user['_id'],\n", " type_item='competition_req',\n", " title='Новая заявка на конкурс',\n", " link=link,\n", " text=f'«{competition['title']}» От {request['user']['username']}'\n", " ))\n", " if PROD:\n", " email_task.notify.delay(user['email'], {\n", " 'list':[[{'label':'Название','text':i['title'], 'img':f\"{FULL_DOMAIN}/b/posts/{i['medias'][0]}\"},{'label':'Размер','text':i['size']},{'label':'Описание','text':i['description']}] for i in request['posts']],\n", " 'text': f'Картины на конкурс «{competition['title']}»:',\n", " 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n", " 'title': f\"От {request['user']['username']}\",\n", " 'login_link': login_link\n", " },\n", " 'Новая заявка на конкурс', 'main.html')" ], "display_name": ".venv" } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }