Merge pull request 'production' (#14) from production into main
This commit is contained in:
commit
c8ee828804
170
build.ipynb
170
build.ipynb
@ -695,6 +695,23 @@
|
|||||||
"## RSYNC"
|
"## RSYNC"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "ead8aa36",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"%%bash\n",
|
||||||
|
"ssh kultmost 'mkdir -p /var/lib/app/.gnupg'\n",
|
||||||
|
"ssh kultmost 'chmod 700 /var/lib/app/.gnupg'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"id": "0a429fca",
|
"id": "0a429fca",
|
||||||
@ -817,6 +834,35 @@
|
|||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "434ecf4b",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"%%bash\n",
|
||||||
|
"ssh kultmost 'tee /app/production/start-uvicorn-back.sh >/dev/null' <<'EOF'\n",
|
||||||
|
"#!/usr/bin/bash\n",
|
||||||
|
"set -euo pipefail\n",
|
||||||
|
"\n",
|
||||||
|
"workers=\"$(nproc)\"\n",
|
||||||
|
"\n",
|
||||||
|
"exec /app/production/.venv/bin/uvicorn main:app \\\n",
|
||||||
|
" --host 127.0.0.1 \\\n",
|
||||||
|
" --port 30080 \\\n",
|
||||||
|
" --workers \"$workers\"\n",
|
||||||
|
"EOF\n",
|
||||||
|
"\n",
|
||||||
|
"ssh kultmost 'chmod +x /app/production/start-uvicorn-back.sh'\n",
|
||||||
|
"ssh kultmost 'ls -ln /app/production/start-uvicorn-back.sh'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
@ -828,23 +874,24 @@
|
|||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# %%bash\n",
|
"%%bash\n",
|
||||||
"# ssh kultmost 'tee /etc/systemd/system/fastapi.service >/dev/null' <<'EOF'\n",
|
"ssh kultmost 'tee /etc/systemd/system/fastapi.service >/dev/null' <<'EOF'\n",
|
||||||
"# [Unit]\n",
|
"[Unit]\n",
|
||||||
"# Description=FastAPI prod service\n",
|
"Description=FastAPI prod service\n",
|
||||||
"# After=network.target\n",
|
"After=network.target\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# [Service]\n",
|
"[Service]\n",
|
||||||
"# WorkingDirectory=/app/production\n",
|
"WorkingDirectory=/app/production\n",
|
||||||
"# ExecStart=/app/production/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30080 --workers 8\n",
|
"ExecStart=/app/production/start-uvicorn-back.sh\n",
|
||||||
"# Restart=always\n",
|
"Restart=always\n",
|
||||||
"# EnvironmentFile=/app/production/env\n",
|
"EnvironmentFile=/app/production/env\n",
|
||||||
|
"CPUWeight=60\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# [Install]\n",
|
"[Install]\n",
|
||||||
"# WantedBy=default.target\n",
|
"WantedBy=multi-user.target\n",
|
||||||
"# EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
|
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
@ -960,9 +1007,75 @@
|
|||||||
"id": "0474516d",
|
"id": "0474516d",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"### restart fastapi"
|
"### restart production "
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5d35489f",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!ssh kultmost 'systemctl start fastapi.service'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "c477dcdd",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!ssh kultmost 'systemctl status fastapi.service'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "1eccaed4",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"%%bash\n",
|
||||||
|
"ssh kultmost 'tee /etc/nginx/sites-enabled/avtopost_back >/dev/null' <<'EOF'\n",
|
||||||
|
"upstream backend_avtopost_servers {\n",
|
||||||
|
" server 127.0.0.1:30080;\n",
|
||||||
|
"}\n",
|
||||||
|
"EOF\n",
|
||||||
|
"ssh kultmost 'nginx -t'\n"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "11a407c2",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!ssh kultmost 'nginx -s reload'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
@ -996,7 +1109,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"id": "6fdc2d37",
|
"id": "1cef4109",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"languageId": "shellscript"
|
"languageId": "shellscript"
|
||||||
@ -1004,7 +1117,28 @@
|
|||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# !ssh kultmost 'systemctl restart fastapi.service'"
|
"%%bash\n",
|
||||||
|
"ssh kultmost 'tee /etc/nginx/sites-enabled/avtopost_back >/dev/null' <<'EOF'\n",
|
||||||
|
"upstream backend_avtopost_servers {\n",
|
||||||
|
" server 127.0.0.1:30081;\n",
|
||||||
|
"}\n",
|
||||||
|
"EOF\n",
|
||||||
|
"ssh kultmost 'nginx -t'"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "42b4958a",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!ssh kultmost 'nginx -s reload'"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
@ -1019,7 +1153,7 @@
|
|||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# !ssh kultmost 'systemctl status fastapi.service'"
|
"!ssh kultmost 'systemctl stop fastapi.service'"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -125,7 +125,7 @@ async def accept_delivery(
|
|||||||
|
|
||||||
new_doc = {
|
new_doc = {
|
||||||
**existing,
|
**existing,
|
||||||
"status": "Получено",
|
"status": "Получено галерей" if 'partner' in current_user.get('roles', []) else "Получено художником",
|
||||||
"notify": notify.upserted_id,
|
"notify": notify.upserted_id,
|
||||||
"received_at": datetime.utcnow(),
|
"received_at": datetime.utcnow(),
|
||||||
"updated_at": datetime.utcnow(),
|
"updated_at": datetime.utcnow(),
|
||||||
|
|||||||
274
email_task.ipynb
274
email_task.ipynb
@ -38,7 +38,8 @@
|
|||||||
" load_env('env')\n",
|
" load_env('env')\n",
|
||||||
"except:\n",
|
"except:\n",
|
||||||
" load_env('env_example')"
|
" load_env('env_example')"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -48,17 +49,33 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"import smtplib\n",
|
"import smtplib\n",
|
||||||
|
"import os\n",
|
||||||
|
"from email.mime.application import MIMEApplication\n",
|
||||||
"from email.mime.multipart import MIMEMultipart\n",
|
"from email.mime.multipart import MIMEMultipart\n",
|
||||||
"from email.mime.text import MIMEText\n",
|
"from email.mime.text import MIMEText\n",
|
||||||
|
"from email.utils import formatdate, make_msgid\n",
|
||||||
|
"from typing import Optional\n",
|
||||||
|
"import gnupg\n",
|
||||||
"from celery import Celery\n",
|
"from celery import Celery\n",
|
||||||
"from config import *\n",
|
"from config import *\n",
|
||||||
"from db import *\n",
|
"from db import *\n",
|
||||||
"from templates import templates\n",
|
"from templates import templates\n",
|
||||||
"import logging\n",
|
"import logging\n",
|
||||||
"from typing import Optional\n",
|
"import asyncio \n"
|
||||||
"import asyncio \n",
|
],
|
||||||
"from email.utils import formatdate, make_msgid\n"
|
"display_name": ".venv"
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "ea1066a5",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:\n",
|
||||||
|
" return f\"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}\"\n"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -67,37 +84,211 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:\n",
|
"GPG_HOME = os.getenv(\"GPG_HOME\", \"/var/lib/app/.gnupg\")\n",
|
||||||
" return f\"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}\"\n",
|
"GPG_BINARY = os.getenv(\"GPG_BINARY\", \"gpg\")\n",
|
||||||
|
"\n",
|
||||||
|
"gpg = gnupg.GPG(\n",
|
||||||
|
" gnupghome=GPG_HOME,\n",
|
||||||
|
" gpgbinary=GPG_BINARY,\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def build_unsubscribe_url(\n",
|
||||||
|
" to_email: str,\n",
|
||||||
|
" user_id: Optional[int],\n",
|
||||||
|
" domain: str,\n",
|
||||||
|
") -> str:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Replace this implementation with your real unsubscribe URL builder.\n",
|
||||||
|
" Avoid putting a raw email address in the URL in production.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" from urllib.parse import quote\n",
|
||||||
|
"\n",
|
||||||
|
" return f\"https://{domain}/unsubscribe?email={quote(to_email)}\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def import_public_key(public_key: str) -> str:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Import an ASCII-armored public key and return its fingerprint.\n",
|
||||||
|
"\n",
|
||||||
|
" The database value should contain the complete public key, for example:\n",
|
||||||
|
"\n",
|
||||||
|
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n",
|
||||||
|
" ...\n",
|
||||||
|
" -----END PGP PUBLIC KEY BLOCK-----\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" result = gpg.import_keys(public_key)\n",
|
||||||
|
"\n",
|
||||||
|
" if not result.fingerprints:\n",
|
||||||
|
" raise RuntimeError(\n",
|
||||||
|
" f\"Unable to import the recipient's public key: {result}\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" # Use the imported fingerprint rather than the email address. The email\n",
|
||||||
|
" # address in the key's UID may not exactly match the recipient address.\n",
|
||||||
|
" return result.fingerprints[0]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def encrypt_mime_message(\n",
|
||||||
|
" message,\n",
|
||||||
|
" recipient_fingerprint: str,\n",
|
||||||
|
"):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Encrypt an already-built MIME message and return a PGP/MIME message.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
" # Serialize the original MIME message before encryption.\n",
|
||||||
|
" plaintext = message.as_bytes()\n",
|
||||||
|
"\n",
|
||||||
|
" # Binary OpenPGP data is appropriate for application/octet-stream.\n",
|
||||||
|
" encrypted = gpg.encrypt(\n",
|
||||||
|
" plaintext,\n",
|
||||||
|
" recipients=[recipient_fingerprint],\n",
|
||||||
|
" armor=False,\n",
|
||||||
|
" always_trust=False,\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" if not encrypted.ok:\n",
|
||||||
|
" raise RuntimeError(\n",
|
||||||
|
" f\"GPG encryption failed: {encrypted.status}; \"\n",
|
||||||
|
" f\"stderr: {encrypted.stderr}\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" encrypted_bytes = encrypted.data\n",
|
||||||
|
"\n",
|
||||||
|
" if not encrypted_bytes:\n",
|
||||||
|
" raise RuntimeError(\"GPG returned an empty encrypted message\")\n",
|
||||||
|
"\n",
|
||||||
|
" encrypted_message = MIMEMultipart(\n",
|
||||||
|
" \"encrypted\",\n",
|
||||||
|
" protocol=\"application/pgp-encrypted\",\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" # Copy normal mail headers. Content-Type and MIME-Version are generated\n",
|
||||||
|
" # automatically for the new multipart/encrypted message.\n",
|
||||||
|
" for header in (\n",
|
||||||
|
" \"From\",\n",
|
||||||
|
" \"To\",\n",
|
||||||
|
" \"Subject\",\n",
|
||||||
|
" \"Message-ID\",\n",
|
||||||
|
" \"Date\",\n",
|
||||||
|
" \"X-Priority\",\n",
|
||||||
|
" \"Importance\",\n",
|
||||||
|
" \"X-Auto-Response-Suppress\",\n",
|
||||||
|
" \"List-Unsubscribe\",\n",
|
||||||
|
" \"List-Unsubscribe-Post\",\n",
|
||||||
|
" ):\n",
|
||||||
|
" value = message.get(header)\n",
|
||||||
|
" if value is not None:\n",
|
||||||
|
" encrypted_message[header] = value\n",
|
||||||
|
"\n",
|
||||||
|
" # Required PGP/MIME version part.\n",
|
||||||
|
" version_part = MIMEApplication(\n",
|
||||||
|
" b\"Version: 1\\r\\n\",\n",
|
||||||
|
" _subtype=\"pgp-encrypted\",\n",
|
||||||
|
" )\n",
|
||||||
|
" version_part.add_header(\n",
|
||||||
|
" \"Content-Description\",\n",
|
||||||
|
" \"PGP/MIME version identification\",\n",
|
||||||
|
" )\n",
|
||||||
|
" encrypted_message.attach(version_part)\n",
|
||||||
|
"\n",
|
||||||
|
" # Encrypted MIME payload.\n",
|
||||||
|
" encrypted_part = MIMEApplication(\n",
|
||||||
|
" encrypted_bytes,\n",
|
||||||
|
" _subtype=\"octet-stream\",\n",
|
||||||
|
" )\n",
|
||||||
|
" encrypted_part.add_header(\n",
|
||||||
|
" \"Content-Description\",\n",
|
||||||
|
" \"OpenPGP encrypted message\",\n",
|
||||||
|
" )\n",
|
||||||
|
" encrypted_part.add_header(\n",
|
||||||
|
" \"Content-Disposition\",\n",
|
||||||
|
" \"inline\",\n",
|
||||||
|
" filename=\"encrypted.asc\",\n",
|
||||||
|
" )\n",
|
||||||
|
" encrypted_message.attach(encrypted_part)\n",
|
||||||
|
"\n",
|
||||||
|
" return encrypted_message\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"async def create_email_message(\n",
|
"async def create_email_message(\n",
|
||||||
" to_email: str,\n",
|
" to_email: str,\n",
|
||||||
" subject: str,\n",
|
" subject: str,\n",
|
||||||
" html_content: str,\n",
|
" html_content: str,\n",
|
||||||
" user_id: int = None,\n",
|
" user_id: Optional[int] = None,\n",
|
||||||
"):\n",
|
"):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Create an HTML email.\n",
|
||||||
|
"\n",
|
||||||
|
" If the recipient has a public GPG key in the database, the complete\n",
|
||||||
|
" MIME message is encrypted using PGP/MIME.\n",
|
||||||
|
"\n",
|
||||||
|
" Expected database structure:\n",
|
||||||
|
"\n",
|
||||||
|
" {\n",
|
||||||
|
" \"email\": \"recipient@example.com\",\n",
|
||||||
|
" \"gnupg\": \"-----BEGIN PGP PUBLIC KEY BLOCK-----\\\\n...\"\n",
|
||||||
|
" }\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
"\n",
|
||||||
" msg = MIMEMultipart(\"alternative\")\n",
|
" msg = MIMEMultipart(\"alternative\")\n",
|
||||||
" \n",
|
"\n",
|
||||||
" msg['From'] = f\"{FROM_NAME} <{FROM_EMAIL}>\"\n",
|
" msg[\"From\"] = f\"{FROM_NAME} <{FROM_EMAIL}>\"\n",
|
||||||
" msg['To'] = to_email\n",
|
" msg[\"To\"] = to_email\n",
|
||||||
" msg['Subject'] = subject\n",
|
" msg[\"Subject\"] = subject\n",
|
||||||
" msg['Message-ID'] = make_msgid(domain=FROM_EMAIL.split('@')[1])\n",
|
" msg[\"Message-ID\"] = make_msgid(\n",
|
||||||
" msg['Date'] = formatdate(localtime=True)\n",
|
" domain=FROM_EMAIL.rsplit(\"@\", 1)[1],\n",
|
||||||
" msg['X-Priority'] = \"3\"\n",
|
" )\n",
|
||||||
" msg['Importance'] = \"normal\"\n",
|
" msg[\"Date\"] = formatdate(localtime=True)\n",
|
||||||
" msg['X-Auto-Response-Suppress'] = \"All\"\n",
|
" msg[\"X-Priority\"] = \"3\"\n",
|
||||||
" \n",
|
" msg[\"Importance\"] = \"normal\"\n",
|
||||||
" # token = await create_unsubscribe_token(to_email, user_id)\n",
|
" msg[\"X-Auto-Response-Suppress\"] = \"All\"\n",
|
||||||
" unsubscribe_url = build_unsubscribe_url(to_email, None, FULL_DOMAIN)\n",
|
"\n",
|
||||||
" if to_email != FROM_EMAIL:\n",
|
" # Do not send unsubscribe headers to yourself.\n",
|
||||||
" msg['List-Unsubscribe'] = f\"<{unsubscribe_url}>\"\n",
|
" if to_email.lower() != FROM_EMAIL.lower():\n",
|
||||||
" msg['List-Unsubscribe-Post'] = \"List-Unsubscribe=One-Click\"\n",
|
" unsubscribe_url = build_unsubscribe_url(\n",
|
||||||
" \n",
|
" to_email=to_email,\n",
|
||||||
" msg.attach(MIMEText(html_content, 'html'))\n",
|
" user_id=user_id,\n",
|
||||||
" \n",
|
" domain=FULL_DOMAIN,\n",
|
||||||
" return msg\n"
|
" )\n",
|
||||||
]
|
"\n",
|
||||||
|
" msg[\"List-Unsubscribe\"] = f\"<{unsubscribe_url}>\"\n",
|
||||||
|
" msg[\"List-Unsubscribe-Post\"] = \"List-Unsubscribe=One-Click\"\n",
|
||||||
|
"\n",
|
||||||
|
" msg.attach(\n",
|
||||||
|
" MIMEText(\n",
|
||||||
|
" html_content,\n",
|
||||||
|
" \"html\",\n",
|
||||||
|
" \"utf-8\",\n",
|
||||||
|
" )\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" # Fetch the recipient's public key.\n",
|
||||||
|
" user = await users_collection.find_one(\n",
|
||||||
|
" {\"email\": to_email},\n",
|
||||||
|
" {\"gnupg\": 1},\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" public_key = user.get(\"gnupg\") if user else None\n",
|
||||||
|
"\n",
|
||||||
|
" if not public_key:\n",
|
||||||
|
" # Return a normal unencrypted email when no key is available.\n",
|
||||||
|
" return msg\n",
|
||||||
|
"\n",
|
||||||
|
" if not isinstance(public_key, str):\n",
|
||||||
|
" raise TypeError(\"The gnupg database field must be a string\")\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" fingerprint = import_public_key(public_key)\n",
|
||||||
|
" return encrypt_mime_message(msg, fingerprint)\n",
|
||||||
|
"\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" # Encryption is mandatory whenever a key exists.\n",
|
||||||
|
" # Do not silently send the message unencrypted.\n",
|
||||||
|
" raise\n"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -140,7 +331,8 @@
|
|||||||
" f\"Subject: {subject}, Error: {str(e)}\"\n",
|
" f\"Subject: {subject}, Error: {str(e)}\"\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
" return False\n"
|
" return False\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -150,7 +342,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"celery_app_email = Celery('emails', broker=f'{redis_uri}2', backend=f'{redis_uri}2', task_track_started=True)\n"
|
"celery_app_email = Celery('emails', broker=f'{redis_uri}2', backend=f'{redis_uri}2', task_track_started=True)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -189,7 +382,8 @@
|
|||||||
" f\"Ошибка при отправке email на {to_email} (попытка {self.request.retries + 1}): {exc}\"\n",
|
" f\"Ошибка при отправке email на {to_email} (попытка {self.request.retries + 1}): {exc}\"\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
" raise self.retry(exc=exc)\n"
|
" raise self.retry(exc=exc)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -199,6 +393,18 @@
|
|||||||
"## Run"
|
"## Run"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3aeae7e8",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!mkdir -p /var/lib/app/.gnupg\n",
|
||||||
|
"!chmod 700 /var/lib/app/.gnupg"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
@ -228,7 +434,8 @@
|
|||||||
"\n",
|
"\n",
|
||||||
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
|
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
|
||||||
" f.write('\\n\\n'.join(app))\n"
|
" f.write('\\n\\n'.join(app))\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -238,7 +445,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!celery -A email_task.celery_app_email worker --loglevel=info"
|
"!celery -A email_task.celery_app_email worker --loglevel=info"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
224
email_task.py
224
email_task.py
@ -32,48 +32,228 @@ except:
|
|||||||
load_env('env_example')
|
load_env('env_example')
|
||||||
|
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import os
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import formatdate, make_msgid
|
||||||
|
from typing import Optional
|
||||||
|
import gnupg
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from config import *
|
from config import *
|
||||||
from db import *
|
from db import *
|
||||||
from templates import templates
|
from templates import templates
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from email.utils import formatdate, make_msgid
|
|
||||||
|
|
||||||
|
|
||||||
def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:
|
def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:
|
||||||
return f"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}"
|
return f"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}"
|
||||||
|
|
||||||
|
|
||||||
|
GPG_HOME = os.getenv("GPG_HOME", "/var/lib/app/.gnupg")
|
||||||
|
GPG_BINARY = os.getenv("GPG_BINARY", "gpg")
|
||||||
|
|
||||||
|
gpg = gnupg.GPG(
|
||||||
|
gnupghome=GPG_HOME,
|
||||||
|
gpgbinary=GPG_BINARY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_unsubscribe_url(
|
||||||
|
to_email: str,
|
||||||
|
user_id: Optional[int],
|
||||||
|
domain: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Replace this implementation with your real unsubscribe URL builder.
|
||||||
|
Avoid putting a raw email address in the URL in production.
|
||||||
|
"""
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
return f"https://{domain}/unsubscribe?email={quote(to_email)}"
|
||||||
|
|
||||||
|
|
||||||
|
def import_public_key(public_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Import an ASCII-armored public key and return its fingerprint.
|
||||||
|
|
||||||
|
The database value should contain the complete public key, for example:
|
||||||
|
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
...
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
"""
|
||||||
|
result = gpg.import_keys(public_key)
|
||||||
|
|
||||||
|
if not result.fingerprints:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Unable to import the recipient's public key: {result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the imported fingerprint rather than the email address. The email
|
||||||
|
# address in the key's UID may not exactly match the recipient address.
|
||||||
|
return result.fingerprints[0]
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_mime_message(
|
||||||
|
message,
|
||||||
|
recipient_fingerprint: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Encrypt an already-built MIME message and return a PGP/MIME message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Serialize the original MIME message before encryption.
|
||||||
|
plaintext = message.as_bytes()
|
||||||
|
|
||||||
|
# Binary OpenPGP data is appropriate for application/octet-stream.
|
||||||
|
encrypted = gpg.encrypt(
|
||||||
|
plaintext,
|
||||||
|
recipients=[recipient_fingerprint],
|
||||||
|
armor=False,
|
||||||
|
always_trust=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not encrypted.ok:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"GPG encryption failed: {encrypted.status}; "
|
||||||
|
f"stderr: {encrypted.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
encrypted_bytes = encrypted.data
|
||||||
|
|
||||||
|
if not encrypted_bytes:
|
||||||
|
raise RuntimeError("GPG returned an empty encrypted message")
|
||||||
|
|
||||||
|
encrypted_message = MIMEMultipart(
|
||||||
|
"encrypted",
|
||||||
|
protocol="application/pgp-encrypted",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy normal mail headers. Content-Type and MIME-Version are generated
|
||||||
|
# automatically for the new multipart/encrypted message.
|
||||||
|
for header in (
|
||||||
|
"From",
|
||||||
|
"To",
|
||||||
|
"Subject",
|
||||||
|
"Message-ID",
|
||||||
|
"Date",
|
||||||
|
"X-Priority",
|
||||||
|
"Importance",
|
||||||
|
"X-Auto-Response-Suppress",
|
||||||
|
"List-Unsubscribe",
|
||||||
|
"List-Unsubscribe-Post",
|
||||||
|
):
|
||||||
|
value = message.get(header)
|
||||||
|
if value is not None:
|
||||||
|
encrypted_message[header] = value
|
||||||
|
|
||||||
|
# Required PGP/MIME version part.
|
||||||
|
version_part = MIMEApplication(
|
||||||
|
b"Version: 1\r\n",
|
||||||
|
_subtype="pgp-encrypted",
|
||||||
|
)
|
||||||
|
version_part.add_header(
|
||||||
|
"Content-Description",
|
||||||
|
"PGP/MIME version identification",
|
||||||
|
)
|
||||||
|
encrypted_message.attach(version_part)
|
||||||
|
|
||||||
|
# Encrypted MIME payload.
|
||||||
|
encrypted_part = MIMEApplication(
|
||||||
|
encrypted_bytes,
|
||||||
|
_subtype="octet-stream",
|
||||||
|
)
|
||||||
|
encrypted_part.add_header(
|
||||||
|
"Content-Description",
|
||||||
|
"OpenPGP encrypted message",
|
||||||
|
)
|
||||||
|
encrypted_part.add_header(
|
||||||
|
"Content-Disposition",
|
||||||
|
"inline",
|
||||||
|
filename="encrypted.asc",
|
||||||
|
)
|
||||||
|
encrypted_message.attach(encrypted_part)
|
||||||
|
|
||||||
|
return encrypted_message
|
||||||
|
|
||||||
|
|
||||||
async def create_email_message(
|
async def create_email_message(
|
||||||
to_email: str,
|
to_email: str,
|
||||||
subject: str,
|
subject: str,
|
||||||
html_content: str,
|
html_content: str,
|
||||||
user_id: int = None,
|
user_id: Optional[int] = None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Create an HTML email.
|
||||||
|
|
||||||
|
If the recipient has a public GPG key in the database, the complete
|
||||||
|
MIME message is encrypted using PGP/MIME.
|
||||||
|
|
||||||
|
Expected database structure:
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "recipient@example.com",
|
||||||
|
"gnupg": "-----BEGIN PGP PUBLIC KEY BLOCK-----\\n..."
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
|
|
||||||
msg['From'] = f"{FROM_NAME} <{FROM_EMAIL}>"
|
msg["From"] = f"{FROM_NAME} <{FROM_EMAIL}>"
|
||||||
msg['To'] = to_email
|
msg["To"] = to_email
|
||||||
msg['Subject'] = subject
|
msg["Subject"] = subject
|
||||||
msg['Message-ID'] = make_msgid(domain=FROM_EMAIL.split('@')[1])
|
msg["Message-ID"] = make_msgid(
|
||||||
msg['Date'] = formatdate(localtime=True)
|
domain=FROM_EMAIL.rsplit("@", 1)[1],
|
||||||
msg['X-Priority'] = "3"
|
)
|
||||||
msg['Importance'] = "normal"
|
msg["Date"] = formatdate(localtime=True)
|
||||||
msg['X-Auto-Response-Suppress'] = "All"
|
msg["X-Priority"] = "3"
|
||||||
|
msg["Importance"] = "normal"
|
||||||
# token = await create_unsubscribe_token(to_email, user_id)
|
msg["X-Auto-Response-Suppress"] = "All"
|
||||||
unsubscribe_url = build_unsubscribe_url(to_email, None, FULL_DOMAIN)
|
|
||||||
if to_email != FROM_EMAIL:
|
# Do not send unsubscribe headers to yourself.
|
||||||
msg['List-Unsubscribe'] = f"<{unsubscribe_url}>"
|
if to_email.lower() != FROM_EMAIL.lower():
|
||||||
msg['List-Unsubscribe-Post'] = "List-Unsubscribe=One-Click"
|
unsubscribe_url = build_unsubscribe_url(
|
||||||
|
to_email=to_email,
|
||||||
msg.attach(MIMEText(html_content, 'html'))
|
user_id=user_id,
|
||||||
|
domain=FULL_DOMAIN,
|
||||||
return msg
|
)
|
||||||
|
|
||||||
|
msg["List-Unsubscribe"] = f"<{unsubscribe_url}>"
|
||||||
|
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
|
||||||
|
|
||||||
|
msg.attach(
|
||||||
|
MIMEText(
|
||||||
|
html_content,
|
||||||
|
"html",
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch the recipient's public key.
|
||||||
|
user = await users_collection.find_one(
|
||||||
|
{"email": to_email},
|
||||||
|
{"gnupg": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
public_key = user.get("gnupg") if user else None
|
||||||
|
|
||||||
|
if not public_key:
|
||||||
|
# Return a normal unencrypted email when no key is available.
|
||||||
|
return msg
|
||||||
|
|
||||||
|
if not isinstance(public_key, str):
|
||||||
|
raise TypeError("The gnupg database field must be a string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fingerprint = import_public_key(public_key)
|
||||||
|
return encrypt_mime_message(msg, fingerprint)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Encryption is mandatory whenever a key exists.
|
||||||
|
# Do not silently send the message unencrypted.
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def connect(email_server, email_port):
|
def connect(email_server, email_port):
|
||||||
|
|||||||
53
main.ipynb
53
main.ipynb
@ -180,23 +180,42 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"def get_delivery_aggregate(_filter: dict = {}, _status: str = None):\n",
|
"from typing import Any, AsyncIterator\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def get_delivery_aggregate(\n",
|
||||||
|
" _filter: dict[str, Any] | None = None,\n",
|
||||||
|
" _status: str | list[str] | None = None,\n",
|
||||||
|
"):\n",
|
||||||
" cursor = delivery_collection.aggregate([\n",
|
" cursor = delivery_collection.aggregate([\n",
|
||||||
" {\"$match\": {\n",
|
" {\n",
|
||||||
" \"prod\": PROD, **_filter\n",
|
" \"$match\": {\n",
|
||||||
" }},\n",
|
" \"prod\": PROD,\n",
|
||||||
|
" **(_filter or {}),\n",
|
||||||
|
" }\n",
|
||||||
|
" },\n",
|
||||||
" {\"$sort\": {\"_id\": -1}},\n",
|
" {\"$sort\": {\"_id\": -1}},\n",
|
||||||
" {\"$group\": {\n",
|
" {\n",
|
||||||
" \"_id\": \"$post_id\",\n",
|
" \"$group\": {\n",
|
||||||
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
|
" \"_id\": \"$post_id\",\n",
|
||||||
" }},\n",
|
" \"doc\": {\"$first\": \"$$ROOT\"},\n",
|
||||||
|
" }\n",
|
||||||
|
" },\n",
|
||||||
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
|
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
|
||||||
" {\"$sort\": {\"_id\": -1}},\n",
|
" {\"$sort\": {\"_id\": -1}},\n",
|
||||||
" ])\n",
|
" ])\n",
|
||||||
" if _status:\n",
|
"\n",
|
||||||
" return (doc async for doc in cursor if doc.get('status') == _status)\n",
|
" if _status is None:\n",
|
||||||
" else:\n",
|
" return cursor\n",
|
||||||
" return cursor\n"
|
"\n",
|
||||||
|
" statuses = {_status} if isinstance(_status, str) else set(_status)\n",
|
||||||
|
"\n",
|
||||||
|
" async def filtered_cursor() -> AsyncIterator[dict[str, Any]]:\n",
|
||||||
|
" async for doc in cursor:\n",
|
||||||
|
" if doc.get(\"status\") in statuses:\n",
|
||||||
|
" yield doc\n",
|
||||||
|
"\n",
|
||||||
|
" return filtered_cursor()\n"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
@ -1689,8 +1708,10 @@
|
|||||||
"):\n",
|
"):\n",
|
||||||
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
|
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
|
||||||
" if exhibition:\n",
|
" if exhibition:\n",
|
||||||
" posts = [doc async for doc in get_delivery_aggregate({\"exhibition_id\": exhibition_id}, 'На выставке')]\n",
|
" posts = [doc async for doc in get_delivery_aggregate({\"exhibition_id\": exhibition_id}, ['Получено', 'Получено галерей', 'Продано', 'На выставке'])]\n",
|
||||||
" \n",
|
" for i in posts:\n",
|
||||||
|
" if (post_id:=i.get('post_id')) and 'post' not in i:\n",
|
||||||
|
" i['post'] = await posts_collection.find_one({'_id': ObjectId(post_id)})\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request, name=\"exhibition/index.html\", context={\"id\": 1, \"user\": user, \"exhibition\": exhibition, \"posts\": posts, 'now': datetime.now()}\n",
|
" request=request, name=\"exhibition/index.html\", context={\"id\": 1, \"user\": user, \"exhibition\": exhibition, \"posts\": posts, 'now': datetime.now()}\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
@ -4998,8 +5019,8 @@
|
|||||||
" \n",
|
" \n",
|
||||||
" data = [doc async for doc in get_delivery_aggregate(_match)]\n",
|
" data = [doc async for doc in get_delivery_aggregate(_match)]\n",
|
||||||
"\n",
|
"\n",
|
||||||
" exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list()\n",
|
" exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).sort(\"_id\", -1).to_list()\n",
|
||||||
" competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list()\n",
|
" competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).sort(\"_id\", -1).to_list()\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request,\n",
|
" request=request,\n",
|
||||||
" name=\"profile/delivery.html\",\n",
|
" name=\"profile/delivery.html\",\n",
|
||||||
|
|||||||
51
main.py
51
main.py
@ -81,24 +81,43 @@ 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)
|
return ''.join(RUS_TO_LAT.get(ch, '' if ch not in string.ascii_letters else ch) for ch in text)
|
||||||
|
|
||||||
|
|
||||||
def get_delivery_aggregate(_filter: dict = {}, _status: str = None):
|
from typing import Any, AsyncIterator
|
||||||
|
|
||||||
|
|
||||||
|
def get_delivery_aggregate(
|
||||||
|
_filter: dict[str, Any] | None = None,
|
||||||
|
_status: str | list[str] | None = None,
|
||||||
|
):
|
||||||
cursor = delivery_collection.aggregate([
|
cursor = delivery_collection.aggregate([
|
||||||
{"$match": {
|
{
|
||||||
"prod": PROD, **_filter
|
"$match": {
|
||||||
}},
|
"prod": PROD,
|
||||||
|
**(_filter or {}),
|
||||||
|
}
|
||||||
|
},
|
||||||
{"$sort": {"_id": -1}},
|
{"$sort": {"_id": -1}},
|
||||||
{"$group": {
|
{
|
||||||
"_id": "$post_id",
|
"$group": {
|
||||||
"doc": {"$first": "$$ROOT"}
|
"_id": "$post_id",
|
||||||
}},
|
"doc": {"$first": "$$ROOT"},
|
||||||
|
}
|
||||||
|
},
|
||||||
{"$replaceRoot": {"newRoot": "$doc"}},
|
{"$replaceRoot": {"newRoot": "$doc"}},
|
||||||
{"$sort": {"_id": -1}},
|
{"$sort": {"_id": -1}},
|
||||||
])
|
])
|
||||||
if _status:
|
|
||||||
return (doc async for doc in cursor if doc.get('status') == _status)
|
if _status is None:
|
||||||
else:
|
|
||||||
return cursor
|
return cursor
|
||||||
|
|
||||||
|
statuses = {_status} if isinstance(_status, str) else set(_status)
|
||||||
|
|
||||||
|
async def filtered_cursor() -> AsyncIterator[dict[str, Any]]:
|
||||||
|
async for doc in cursor:
|
||||||
|
if doc.get("status") in statuses:
|
||||||
|
yield doc
|
||||||
|
|
||||||
|
return filtered_cursor()
|
||||||
|
|
||||||
|
|
||||||
async def get_latest_news() -> List[dict]:
|
async def get_latest_news() -> List[dict]:
|
||||||
cursor = news_collection.find({'publish_on_platform': True}).sort("_id", -1).limit(30)
|
cursor = news_collection.find({'publish_on_platform': True}).sort("_id", -1).limit(30)
|
||||||
@ -1260,8 +1279,10 @@ async def exhibition_members(
|
|||||||
):
|
):
|
||||||
exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})
|
exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})
|
||||||
if exhibition:
|
if exhibition:
|
||||||
posts = [doc async for doc in get_delivery_aggregate({"exhibition_id": exhibition_id}, 'На выставке')]
|
posts = [doc async for doc in get_delivery_aggregate({"exhibition_id": exhibition_id}, ['Получено', 'Получено галерей', 'Продано', 'На выставке'])]
|
||||||
|
for i in posts:
|
||||||
|
if (post_id:=i.get('post_id')) and 'post' not in i:
|
||||||
|
i['post'] = await posts_collection.find_one({'_id': ObjectId(post_id)})
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request, name="exhibition/index.html", context={"id": 1, "user": user, "exhibition": exhibition, "posts": posts, 'now': datetime.now()}
|
request=request, name="exhibition/index.html", context={"id": 1, "user": user, "exhibition": exhibition, "posts": posts, 'now': datetime.now()}
|
||||||
)
|
)
|
||||||
@ -3818,8 +3839,8 @@ async def profile_delivery(
|
|||||||
|
|
||||||
data = [doc async for doc in get_delivery_aggregate(_match)]
|
data = [doc async for doc in get_delivery_aggregate(_match)]
|
||||||
|
|
||||||
exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list()
|
exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).sort("_id", -1).to_list()
|
||||||
competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list()
|
competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).sort("_id", -1).to_list()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="profile/delivery.html",
|
name="profile/delivery.html",
|
||||||
|
|||||||
@ -38,3 +38,4 @@ celery[redis]==5.6.3
|
|||||||
pika==1.3.2
|
pika==1.3.2
|
||||||
pillow==12.1.1
|
pillow==12.1.1
|
||||||
pytz
|
pytz
|
||||||
|
python-gnupg
|
||||||
|
|||||||
@ -97,7 +97,7 @@
|
|||||||
<div class="b_1tx4m">
|
<div class="b_1tx4m">
|
||||||
<picture data-link="https://kultmost.ru/artists/{{post.user._id}}" data-label="Все картины автора и стоимость">
|
<picture data-link="https://kultmost.ru/artists/{{post.user._id}}" data-label="Все картины автора и стоимость">
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
||||||
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}">
|
src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<figcaption class="c_fzh03 b_bp0ws">
|
<figcaption class="c_fzh03 b_bp0ws">
|
||||||
|
|||||||
@ -73,7 +73,7 @@
|
|||||||
<div class="b_1tx4m">
|
<div class="b_1tx4m">
|
||||||
<picture>
|
<picture>
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
||||||
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}">
|
src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<figcaption class="c_fzh03 b_bp0ws">
|
<figcaption class="c_fzh03 b_bp0ws">
|
||||||
|
|||||||
@ -92,11 +92,12 @@
|
|||||||
<div class="c_cnedr">
|
<div class="c_cnedr">
|
||||||
{% for doc in posts %}
|
{% for doc in posts %}
|
||||||
{% set post=doc.get('post')%}
|
{% set post=doc.get('post')%}
|
||||||
|
{% if post %}
|
||||||
<figure class="c_g5532 b_wcxt0">
|
<figure class="c_g5532 b_wcxt0">
|
||||||
<div class="b_1tx4m">
|
<div class="b_1tx4m">
|
||||||
<picture data-link="https://kultmost.ru/artists/{{post.user._id}}" data-label="Все картины автора и стоимость">
|
<picture data-link="https://kultmost.ru/artists/{{post.get('user',{}).get('_id')}}" data-label="Все картины автора и стоимость">
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
|
||||||
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}">
|
src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<figcaption class="c_fzh03 b_bp0ws">
|
<figcaption class="c_fzh03 b_bp0ws">
|
||||||
@ -123,6 +124,7 @@
|
|||||||
<p class="c_tn4fi b_vjrwu">{{ (post._id.generation_time.strftime('%d.%m.%Y')) if post._id else ''}}</p>
|
<p class="c_tn4fi b_vjrwu">{{ (post._id.generation_time.strftime('%d.%m.%Y')) if post._id else ''}}</p>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="b_1vs97">
|
<div class="b_1vs97">
|
||||||
<picture>
|
<picture>
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1vs97"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1vs97"
|
||||||
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}">
|
src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<figcaption class="c_dvi1x b_is70q">
|
<figcaption class="c_dvi1x b_is70q">
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif doc.status == 'Получено' %}
|
{% elif 'Получено' in (doc.status or '') %}
|
||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -101,99 +101,115 @@
|
|||||||
<div class="c_ru432">
|
<div class="c_ru432">
|
||||||
{{ profile_nav.profile_nav(user, 'delivery') }}
|
{{ profile_nav.profile_nav(user, 'delivery') }}
|
||||||
<div class="c_tihtk" id="main">
|
<div class="c_tihtk" id="main">
|
||||||
{% for competition in competitions %}
|
<!-- <details>
|
||||||
<article class="c_9fgdh b_ax8vz" id="l{{competition._id}}">
|
<summary>Конкурсы</summary>
|
||||||
<a href="/profile/delivery/competition/{{competition._id}}#main">
|
<div class="c_tihtk"> -->
|
||||||
<div class="b_ornr9">
|
{% for competition in competitions %}
|
||||||
<picture>
|
<article class="c_9fgdh b_ax8vz" id="l{{competition._id}}">
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ competition.title }}" class="b_ornr9" src="/b/competition/{{ competition.poster_file_id }}">
|
<a href="/profile/delivery/competition/{{competition._id}}#main">
|
||||||
</picture>
|
<div class="b_ornr9">
|
||||||
</div>
|
<picture>
|
||||||
</a>
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ competition.title }}" class="b_ornr9"
|
||||||
|
src="/b/competition/{{ competition.poster_file_id }}">
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
<div class="c_z3dpx b_73p10">
|
<div class="c_z3dpx b_73p10">
|
||||||
<a href="/profile/delivery/competition/{{competition._id}}#main" class="b_73p10">
|
<a href="/profile/delivery/competition/{{competition._id}}#main" class="b_73p10">
|
||||||
<div class="b_vabj9_2">
|
<div class="b_vabj9_2">
|
||||||
<span class="b_0jst1_2">Конкурс</span>
|
<span class="b_0jst1_2">Конкурс</span>
|
||||||
</div>
|
</div>
|
||||||
{% if competition.get('status',-1) == 2 %}
|
{% if competition.get('status',-1) == 2 %}
|
||||||
<div class="b_vabj9_2">
|
<div class="b_vabj9_2">
|
||||||
<span class="b_0jst1_2">Закончился</span>
|
<span class="b_0jst1_2">Закончился</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif competition.get('status',-1) == 1 %}
|
{% elif competition.get('status',-1) == 1 %}
|
||||||
<div class="b_vabj9_2">
|
<div class="b_vabj9_2">
|
||||||
<span class="b_0jst1_2">Идет отбор</span>
|
<span class="b_0jst1_2">Идет отбор</span>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="b_vabj9">
|
<div class="b_vabj9">
|
||||||
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
|
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет
|
||||||
|
отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<header class="c_m876y b_9xtz9">
|
<header class="c_m876y b_9xtz9">
|
||||||
<h3 class="c_hysjg b_8f3uh" style="color: #222;">{{ competition.title }}</h3>
|
<h3 class="c_hysjg b_8f3uh" style="color: #222;">{{ competition.title }}</h3>
|
||||||
</header>
|
</header>
|
||||||
<p class="c_2fr39 b_tfnz2">{{ competition.topic }}</p>
|
<p class="c_2fr39 b_tfnz2">{{ competition.topic }}</p>
|
||||||
{% if profile %}
|
{% if profile %}
|
||||||
<button class="c_aq9et">
|
<button class="c_aq9et">
|
||||||
<span class="c_som9s">Посмотреть заявки</span>
|
<span class="c_som9s">Посмотреть заявки</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set user = competition.get('user') %}
|
{% set user = competition.get('user') %}
|
||||||
<div class="c_dbbe4 b_otttb">
|
<div class="c_dbbe4 b_otttb">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<img class="c_pm80w b_a68fq" src="/a/{{user._id}}" alt="{{ user.username | e }}" crossorigin="anonymous" fetchpriority="high" loading="eager">
|
<img class="c_pm80w b_a68fq" src="/a/{{user._id}}" alt="{{ user.username | e }}" crossorigin="anonymous"
|
||||||
<div class="c_4402p b_ntazj">
|
fetchpriority="high" loading="eager">
|
||||||
<p class="c_kvah4 b_ghosy">{{ user.username | e }}</p>
|
<div class="c_4402p b_ntazj">
|
||||||
</div>
|
<p class="c_kvah4 b_ghosy">{{ user.username | e }}</p>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<p class="c_2fr39 b_tfnz2">Подробнее</p>
|
<p class="c_2fr39 b_tfnz2">Подробнее</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<p class="c_drpqj b_mp1wb">{{ (competition.datetime_start.strftime('%d.%m.%Y')) if competition.datetime_start else ''}}</p>
|
<p class="c_drpqj b_mp1wb">{{ (competition.datetime_start.strftime('%d.%m.%Y')) if competition.datetime_start else
|
||||||
|
''}}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for i in exhibitions %}
|
<!-- </div>
|
||||||
<article class="c_9fgdh b_ax8vz">
|
</details>
|
||||||
<a href="/profile/delivery/exhibition/{{i._id}}#main">
|
<details open>
|
||||||
<div class="b_ornr9">
|
<summary>Выставки</summary>
|
||||||
<picture>
|
<div class="c_tihtk"> -->
|
||||||
<img src="/b/exhibition/{{i.poster_file_id}}" data-src="/b/exhibition/{{i.poster_orig_id}}" id="preview_poster" crossorigin="anonymous" fetchpriority="high" loading="eager" alt="Загрузите афишу" class="b_ornr9">
|
{% for i in exhibitions %}
|
||||||
</picture>
|
<article class="c_9fgdh b_ax8vz">
|
||||||
</div>
|
<a href="/profile/delivery/exhibition/{{i._id}}#main">
|
||||||
</a>
|
<div class="b_ornr9">
|
||||||
<div class="c_z3dpx b_73p10">
|
<picture>
|
||||||
<a class="b_73p10" href="/profile/delivery/exhibition/{{i._id}}#main">
|
<img src="/b/exhibition/{{i.poster_file_id}}" data-src="/b/exhibition/{{i.poster_orig_id}}" id="preview_poster"
|
||||||
<div class="b_vabj9_2">
|
crossorigin="anonymous" fetchpriority="high" loading="eager" alt="Загрузите афишу" class="b_ornr9">
|
||||||
<span class="b_0jst1_2">Выставка</span>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<div class="c_c4wam b_vabj9">
|
</a>
|
||||||
{% if now > i.date_finish %}
|
<div class="c_z3dpx b_73p10">
|
||||||
<span class="c_95csi b_0jst1">Закончилась</span>
|
<a class="b_73p10" href="/profile/delivery/exhibition/{{i._id}}#main">
|
||||||
{% elif now > i.datetime_start %}
|
<div class="b_vabj9_2">
|
||||||
<span class="c_95csi b_0jst1">Началась</span>
|
<span class="b_0jst1_2">Выставка</span>
|
||||||
{% else %}
|
</div>
|
||||||
<span class="c_95csi b_0jst1">Формируется</span>
|
<div class="c_c4wam b_vabj9">
|
||||||
{% endif %}
|
{% if now > i.date_finish %}
|
||||||
</div>
|
<span class="c_95csi b_0jst1">Закончилась</span>
|
||||||
<header class="c_m876y b_9xtz9">
|
{% elif now > i.datetime_start %}
|
||||||
<h3 class="c_hysjg b_8f3uh" id="preview_title" style="color: #222;">{{i.title}}</h3>
|
<span class="c_95csi b_0jst1">Началась</span>
|
||||||
</header>
|
{% else %}
|
||||||
<p class="c_2fr39 b_tfnz2" id="preview_topic">{{i.topic}}</p>
|
<span class="c_95csi b_0jst1">Формируется</span>
|
||||||
<div class="c_dbbe4 b_otttb">
|
{% endif %}
|
||||||
{% if user %}
|
</div>
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" class="c_pm80w b_a68fq" src="/a/{{i.user._id}}" alt="{{ i.user.username | e }}">
|
<header class="c_m876y b_9xtz9">
|
||||||
<div class="c_4402p b_ntazj">
|
<h3 class="c_hysjg b_8f3uh" id="preview_title" style="color: #222;">{{i.title}}</h3>
|
||||||
<p class="c_kvah4 b_ghosy">{{ i.user.username }}</p>
|
</header>
|
||||||
</div>
|
<p class="c_2fr39 b_tfnz2" id="preview_topic">{{i.topic}}</p>
|
||||||
{% endif %}
|
<div class="c_dbbe4 b_otttb">
|
||||||
</div>
|
{% if user %}
|
||||||
<p class="c_2fr39 b_tfnz2">Подробнее</p>
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" class="c_pm80w b_a68fq"
|
||||||
</a>
|
src="/a/{{i.user._id}}" alt="{{ i.user.username | e }}">
|
||||||
<p class="c_drpqj b_mp1wb">{{ ( i._id.generation_time.strftime('%d.%m.%Y')) if i._id else ''}}</p>
|
<div class="c_4402p b_ntazj">
|
||||||
</div>
|
<p class="c_kvah4 b_ghosy">{{ i.user.username }}</p>
|
||||||
</article>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="c_2fr39 b_tfnz2">Подробнее</p>
|
||||||
|
</a>
|
||||||
|
<p class="c_drpqj b_mp1wb">{{ ( i._id.generation_time.strftime('%d.%m.%Y')) if i._id else ''}}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
<!-- </div>
|
||||||
|
</details> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,6 +224,7 @@
|
|||||||
{{ profile_nav.style() }}
|
{{ profile_nav.style() }}
|
||||||
{{card.style()}}
|
{{card.style()}}
|
||||||
<style>
|
<style>
|
||||||
|
summary{ text-align: center; font-size: 24px; color: #5f1798; font-weight: bold; width: 100%; }
|
||||||
/* Base (common) styles */
|
/* Base (common) styles */
|
||||||
.c_aihm6{width: 100%;}
|
.c_aihm6{width: 100%;}
|
||||||
.mw{margin: 0 20px;
|
.mw{margin: 0 20px;
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">Запрос на отправку</span>
|
<span class="b_5dr47">Запрос на отправку</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif doc.status == 'Получено' %}
|
{% elif 'Получено' in (doc.status or '') %}
|
||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif doc.status == 'Получено' %}
|
{% elif 'Получено' in (doc.status or '') %}
|
||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,6 +63,12 @@
|
|||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="c_nb6gy floating-group">
|
||||||
|
<label for="gnupg" class="c_z3ybu floating-label">GnuPG</label>
|
||||||
|
<textarea type="text" id="gnupg" name="gnupg" class="c_4ykpn" ></textarea>
|
||||||
|
<span>Самозащита электронной почты</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="c_aihm6 floating-group">
|
<div class="c_aihm6 floating-group">
|
||||||
<label for="phone" class="c_z3ybu floating-label">Телефон<span style="color:red">*</span></label>
|
<label for="phone" class="c_z3ybu floating-label">Телефон<span style="color:red">*</span></label>
|
||||||
<input name="phone" id="phone" class="c_xhd5k" required />
|
<input name="phone" id="phone" class="c_xhd5k" required />
|
||||||
@ -222,6 +228,7 @@ document.getElementById('living_place').value = `{% if user.get('living_place')
|
|||||||
document.getElementById('social_media').value = `{% if user.get('social_media') is not none %}{{user.get('social_media','') }}{% endif %}`
|
document.getElementById('social_media').value = `{% if user.get('social_media') is not none %}{{user.get('social_media','') }}{% endif %}`
|
||||||
document.getElementById('marketplaces').value = `{% if user.get('marketplaces') is not none %}{{user.get('marketplaces','') }}{% endif %}`
|
document.getElementById('marketplaces').value = `{% if user.get('marketplaces') is not none %}{{user.get('marketplaces','') }}{% endif %}`
|
||||||
document.getElementById('email').value = `{% if user.get('email') is not none %}{{user.get('email','') }}{% endif %}`
|
document.getElementById('email').value = `{% if user.get('email') is not none %}{{user.get('email','') }}{% endif %}`
|
||||||
|
document.getElementById('gnupg').value = `{% if user.get('gnupg') is not none %}{{user.get('gnupg','') }}{% endif %}`
|
||||||
document.getElementById('phone').value = `{% if user.get('phone') is not none %}{{user.get('phone','') }}{% endif %}`
|
document.getElementById('phone').value = `{% if user.get('phone') is not none %}{{user.get('phone','') }}{% endif %}`
|
||||||
document.getElementById('messengers').value = `{% if user.get('messengers') is not none %}{{user.get('messengers','') }}{% endif %}`
|
document.getElementById('messengers').value = `{% if user.get('messengers') is not none %}{{user.get('messengers','') }}{% endif %}`
|
||||||
document.getElementById('specialization').value = `{% if user.get('specialization') is not none %}{{user.get('specialization','') }}{% endif %}`
|
document.getElementById('specialization').value = `{% if user.get('specialization') is not none %}{{user.get('specialization','') }}{% endif %}`
|
||||||
@ -270,6 +277,7 @@ document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById(
|
|||||||
'social_media': document.getElementById('social_media').value,
|
'social_media': document.getElementById('social_media').value,
|
||||||
'marketplaces': document.getElementById('marketplaces').value,
|
'marketplaces': document.getElementById('marketplaces').value,
|
||||||
'email': document.getElementById('email').value,
|
'email': document.getElementById('email').value,
|
||||||
|
'gnupg': document.getElementById('gnupg').value,
|
||||||
'phone': document.getElementById('phone').value,
|
'phone': document.getElementById('phone').value,
|
||||||
'messengers': document.getElementById('messengers').value,
|
'messengers': document.getElementById('messengers').value,
|
||||||
'specialization': document.getElementById('specialization').value,
|
'specialization': document.getElementById('specialization').value,
|
||||||
|
|||||||
@ -12,21 +12,21 @@
|
|||||||
<figure class="c_4b1hy b_uu2ub" data-exhibition_id="{{doc.get('exhibition_id')}}" data-delivery-id="{{doc._id}}" data-sender="{{doc.sender._id}}" data-receiver="{{doc.receiver._id}}">
|
<figure class="c_4b1hy b_uu2ub" data-exhibition_id="{{doc.get('exhibition_id')}}" data-delivery-id="{{doc._id}}" data-sender="{{doc.sender._id}}" data-receiver="{{doc.receiver._id}}">
|
||||||
<img class="b_1fvkt" src="/b/posts/{{doc.img}}">
|
<img class="b_1fvkt" src="/b/posts/{{doc.img}}">
|
||||||
<figcaption class="c_3g8gh b_pvw5a" data-id="{{doc._id}}">
|
<figcaption class="c_3g8gh b_pvw5a" data-id="{{doc._id}}">
|
||||||
{% if doc.status == 'Ожидает отправки' %}
|
{% if (doc.status or '') == 'Ожидает отправки' %}
|
||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{(doc.status or '')}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif doc.status == 'Получено' %}
|
{% elif 'Получено' in (doc.status or '') %}
|
||||||
<div class="b_thlt1">
|
<div class="b_thlt1">
|
||||||
<span class="b_5dr47">{{doc.status}}</span>
|
<span class="b_5dr47">{{(doc.status or '')}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% elif doc.status == 'Ожидает получения' %}
|
{% elif (doc.status or '') == 'Ожидает получения' %}
|
||||||
<div class="b_thlt1_2">
|
<div class="b_thlt1_2">
|
||||||
<span class="b_5dr47_2">{{doc.status}}</span>
|
<span class="b_5dr47_2">{{(doc.status or '')}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="b_thlt1_2">
|
<div class="b_thlt1_2">
|
||||||
<span class="b_5dr47_2">{{doc.status}}</span>
|
<span class="b_5dr47_2">{{(doc.status or '')}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
|
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
|
||||||
|
|||||||
@ -148,7 +148,7 @@
|
|||||||
<div class="b_1fvkt">
|
<div class="b_1fvkt">
|
||||||
<picture>
|
<picture>
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}"
|
||||||
class="b_1fvkt" src="/b/posts/{{ post.medias[0] }}"
|
class="b_1fvkt" src="/b/posts/{{ post.medias_original[0] }}/500/90/80"
|
||||||
data-src="/b/posts/{{ post.medias_original[0] }}">
|
data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -176,7 +176,7 @@ failBtn.addEventListener('click', () => selectMember('fail'));
|
|||||||
<div class="b_1fvkt">
|
<div class="b_1fvkt">
|
||||||
<picture>
|
<picture>
|
||||||
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1fvkt"
|
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1fvkt"
|
||||||
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}">
|
src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<figcaption class="c_3g8gh b_pvw5a">
|
<figcaption class="c_3g8gh b_pvw5a">
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
{% import "competition/list.html" as competition %}
|
{% import "competition/list.html" as competition %}
|
||||||
{% import "components/label.html" as label %}
|
{% import "components/label.html" as label %}
|
||||||
|
|
||||||
{% block title %}Каталог картин{% endblock %}
|
{% block title %}Каталог картин | Выставка {{exhibition.title}}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header.section(user) }}
|
{{ header.section(user) }}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||||
<div class="c_3ihvi" id="main">
|
<div class="c_3ihvi" id="main">
|
||||||
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
||||||
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
|
<span class="c_d2l0b b_al8e7">Отобранные картины (<span id="lenPosts2">{{ exhibition.posts | length }}</span>)</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="c_tihtk">
|
<div class="c_tihtk">
|
||||||
@ -476,6 +476,8 @@ genre = genres.map(g => {
|
|||||||
// Обновляем счётчик
|
// Обновляем счётчик
|
||||||
const lenEl = document.getElementById('lenPosts');
|
const lenEl = document.getElementById('lenPosts');
|
||||||
if (lenEl) lenEl.textContent = exhibition.posts.length;
|
if (lenEl) lenEl.textContent = exhibition.posts.length;
|
||||||
|
const lenEl2 = document.getElementById('lenPosts2');
|
||||||
|
if (lenEl2) lenEl2.textContent = exhibition.posts.length;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
el.checked = !isAdding;
|
el.checked = !isAdding;
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
{% import "components/label.html" as label %}
|
{% import "components/label.html" as label %}
|
||||||
{% import "/components/contacts.html" as contacts %}
|
{% import "/components/contacts.html" as contacts %}
|
||||||
|
|
||||||
{% block title %}Отобранные картины | Выставка {{exhibition.title}}{% endblock %}
|
{% block title %}Отправить приглашения | Выставка {{exhibition.title}}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
{% import "/components/header.html" as header %}
|
{% import "/components/header.html" as header %}
|
||||||
{% import "/components/footer.html" as footer %}
|
{% import "/components/footer.html" as footer %}
|
||||||
{% import "posts/profile_card.html" as card %}
|
{% import "posts/profile_card.html" as card %}
|
||||||
|
{% import "/components/contacts.html" as contacts %}
|
||||||
|
|
||||||
|
|
||||||
{% block title %}Посты{% endblock %}
|
{% block title %}Посты{% endblock %}
|
||||||
@ -14,7 +15,6 @@
|
|||||||
<div class="c_gtoe9">
|
<div class="c_gtoe9">
|
||||||
<div class="c_iiehk">
|
<div class="c_iiehk">
|
||||||
<div class="c_8xkk0 b_2rmrs">
|
<div class="c_8xkk0 b_2rmrs">
|
||||||
|
|
||||||
<div class="b_hjteq">
|
<div class="b_hjteq">
|
||||||
<div class="b_zo7wz" for="avatar">
|
<div class="b_zo7wz" for="avatar">
|
||||||
<img class="b_rhmvx" src="/a/{{find._id}}" alt="{{find.username}}" id="preview" crossorigin="anonymous" fetchpriority="high" loading="eager">
|
<img class="b_rhmvx" src="/a/{{find._id}}" alt="{{find.username}}" id="preview" crossorigin="anonymous" fetchpriority="high" loading="eager">
|
||||||
@ -22,10 +22,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="c_kpaav b_xsvl1">{{find.username}}</p>
|
<p class="c_kpaav b_xsvl1">{{find.username}}</p>
|
||||||
{% if 'admin' in user.get('roles', []) %}
|
{% if 'admin' in user.get('roles', []) %}
|
||||||
<div class="c_ctfod b_289i1" id="switch-user">
|
<div class="c_ctfod b_289i1" id="switch-user">
|
||||||
<button class="c_d2l0b b_al8e7">Переключиться на пользователя</button>
|
<button class="c_d2l0b b_al8e7">Переключиться на пользователя</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if 'admin' in user.get('roles', []) or 'partner' in user.get('roles', []) %}
|
||||||
|
<!-- <div class="c_ctfod b_289i1"><span class="c_d2l0b b_al8e7">Написать пользователю</span></div> -->
|
||||||
|
{{ contacts.details(find) }}
|
||||||
|
{% endif %}
|
||||||
|
<div >На платформе с {{ (find._id.generation_time.strftime('%Y')) if find._id else ''}}</div>
|
||||||
|
{% if find.get('grade') is not none%}
|
||||||
|
<p class="it o">Звание:</p>
|
||||||
|
<p class="it">{{find.grade}}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if find.get('biography') is not none%}
|
||||||
|
<p class="it o">Биография:</p>
|
||||||
|
<p class="it">{{find.biography}}</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>👁 {{total_watch_count}} (+{{today_watch_count}} Сегодня)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c_ru432">
|
||||||
|
<div class="c_tihtk">
|
||||||
|
{% for i in cards %}
|
||||||
|
{{ card.section(i, loop.index, user) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<script>
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
const userId = '{{find._id}}';
|
const userId = '{{find._id}}';
|
||||||
@ -50,33 +75,6 @@ document.getElementById('switch-user').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
// @license-end
|
// @license-end
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
|
||||||
{% if 'admin' in user.get('roles', []) %}
|
|
||||||
<div class="c_ctfod b_289i1"><span class="c_d2l0b b_al8e7">Написать пользователю</span></div>
|
|
||||||
{% endif %}
|
|
||||||
<div >На платформе с {{ (find._id.generation_time.strftime('%Y')) if find._id else ''}}</div>
|
|
||||||
{% if find.get('grade') is not none%}
|
|
||||||
<p class="it o">Звание:</p>
|
|
||||||
<p class="it">{{find.grade}}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if find.get('biography') is not none%}
|
|
||||||
<p class="it o">Биография:</p>
|
|
||||||
<p class="it">{{find.biography}}</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>👁 {{total_watch_count}} (+{{today_watch_count}} Сегодня)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="c_ru432">
|
|
||||||
<div class="c_tihtk">
|
|
||||||
{% for i in cards %}
|
|
||||||
{{ card.section(i, loop.index, user) }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<script>
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -101,6 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ header.style() }}
|
{{ header.style() }}
|
||||||
{{ card.style() }}
|
{{ card.style() }}
|
||||||
|
{{ contacts.style() }}
|
||||||
<style>
|
<style>
|
||||||
.o{font-style: italic;color: #777;}
|
.o{font-style: italic;color: #777;}
|
||||||
.it{padding: 0px 15px;}
|
.it{padding: 0px 15px;}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user