From a31ccaec3891bebc9a93d2fcf60dc701a06f028e698c33ddb12936a511bab752 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Mon, 6 Jul 2026 08:27:19 -0400 Subject: [PATCH] =?UTF-8?q?Bot=20commit:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20tasks.ipynb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks.ipynb | 918 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 918 insertions(+) create mode 100644 tasks.ipynb diff --git a/tasks.ipynb b/tasks.ipynb new file mode 100644 index 0000000..320cc35 --- /dev/null +++ b/tasks.ipynb @@ -0,0 +1,918 @@ +{ + "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')" + ] + }, + { + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019d30b5", + "metadata": {}, + "outputs": [], + "source": [ + "async def notify_in_profile(item: NotifyItem):\n", + " return 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" + ] + }, + { + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbff5107", + "metadata": {}, + "outputs": [], + "source": [ + "async def update_social_post(post):\n", + " pass" + ] + }, + { + "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)" + ] + }, + { + "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)" + ] + }, + { + "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)" + ] + }, + { + "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': 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:{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:{exhibition['_id']}:{social['_id']}\")\n", + " if already_sent:\n", + " print(social)\n", + " client.notify.delay(post_ref, text, attachments, bson_clear(social), sign_avatar)" + ] + }, + { + "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': 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:{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:{competition['_id']}:{social['_id']}\")\n", + " if already_sent:\n", + " print(social)\n", + " client.notify.delay(post_ref, text, attachments, bson_clear(social), sign_avatar)" + ] + }, + { + "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')" + ] + }, + { + "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)" + ] + }, + { + "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" + ] + }, + { + "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')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c8e1473", + "metadata": {}, + "outputs": [], + "source": [ + "async def accept_invitations(invitations_id):\n", + " invitation = await invitations_collection.find_one_and_update(\n", + " {\"_id\": ObjectId(invitations_id)},\n", + " {\"$set\": {\"accept\": True}},\n", + " return_document=True, \n", + " )\n", + " delivery_collection.update_many({'post_id': {'$in': [str(doc['_id']) for doc in invitation.get('posts', [])]}}, {'$set':{'status': 'Ожидает отправки'}})\n", + " await notify_in_profile(NotifyItem(\n", + " user_id=invitation.get('exhibition',{}).get(\"user\",{}).get('_id'),\n", + " type_item='c',\n", + " title=f'Выставка \"{invitation.get('exhibition',{}).get(\"title\")}\"',\n", + " link=f'/profile/delivery',\n", + " text=f'Художник принял условия'\n", + " ))" + ] + }, + { + "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}}).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", + " \"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.title}',\n", + " 'main.html'\n", + " )\n", + " \n" + ] + }, + { + "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/delivery#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", + " \"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/delivery',\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", + " print([i['title'] for i in posts_sorted])\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", + " )" + ] + }, + { + "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')" + ] + } + ], + "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.14.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}