Merge branch 'production'

This commit is contained in:
vladislav 2026-08-17 15:12:53 -04:00
commit d58975f478
Signed by: Vladislav
GPG Key ID: D97C9E5E4860EE45
41 changed files with 1924 additions and 851 deletions

View File

@ -12,8 +12,15 @@
"outputs": [],
"source": [
"!pip install -r requirements.txt"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
"id": "20340e34",
"metadata": {},
"source": [
"## BUILD"
]
},
{
"cell_type": "markdown",
@ -65,8 +72,7 @@
"EOF\n",
"\n",
"chmod +x .git/hooks/pre-commit\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -114,8 +120,56 @@
" load_env('env')\n",
"except:\n",
" load_env('env_example')"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
"id": "74d7ec58",
"metadata": {},
"source": [
"### build (ipynb -> py)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "2809c42c",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import os\n",
"\n",
"IGNORE = ['build.ipynb', 'data.ipynb', 'migrate.ipynb']\n",
"def build():\n",
" for _filename in [i for i in os.listdir() if 'ipynb' in i and i not in IGNORE]:\n",
" filename_without_extension = os.path.splitext(os.path.basename(_filename))[0]\n",
"\n",
" with open(f'{filename_without_extension}.ipynb', 'r') as f:\n",
" data = f.read()\n",
" main = json.loads(data)\n",
" app = []\n",
" for cell in main['cells']:\n",
" if '## Run' in cell['source']:\n",
" break\n",
" if '# ignore\\n' in cell['source']:\n",
" continue\n",
" app.append(''.join(cell['source']))\n",
"\n",
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
" f.write(f\"# `{filename_without_extension}.py` generate from `{filename_without_extension}.ipynb` by `build.ipynb` \\n\")\n",
" f.write('\\n\\n'.join(app))\n",
"\n",
"build()"
]
},
{
"cell_type": "markdown",
"id": "0490beed",
"metadata": {},
"source": [
"## KUBECTL / DOCKER"
]
},
{
"cell_type": "markdown",
@ -141,8 +195,7 @@
"kubectl -n container-registry create secret generic registry-basic-auth \\\n",
" --from-file=auth=/tmp/registry.htpasswd \\\n",
" --dry-run=client -o yaml | kubectl apply -f -\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -156,8 +209,7 @@
"outputs": [],
"source": [
"!kubectl create secret docker-registry regcred --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\"$(git config --global --get user.email)\""
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -172,8 +224,7 @@
"source": [
"!kubectl create namespace kultmost\n",
"!kubectl create secret docker-registry regcred -n kultmost --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\"$(git config --global --get user.email)\""
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -187,8 +238,7 @@
"outputs": [],
"source": [
"!docker login $REGISTRY_HOST -u 'user' -p $REGISTRY_PASS"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -210,8 +260,7 @@
"outputs": [],
"source": [
"!docker pull $REGISTRY_HOST/python:3.13-alpine"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -225,50 +274,8 @@
"outputs": [],
"source": [
"!docker pull $REGISTRY_HOST/python:3.13-slim"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "74d7ec58",
"metadata": {},
"source": [
"### build (ipynb -> py)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2809c42c",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import os\n",
"\n",
"IGNORE = ['build.ipynb', 'data.ipynb', 'migrate.ipynb']\n",
"def build():\n",
" for _filename in [i for i in os.listdir() if 'ipynb' in i and i not in IGNORE]:\n",
" filename_without_extension = os.path.splitext(os.path.basename(_filename))[0]\n",
"\n",
" with open(f'{filename_without_extension}.ipynb', 'r') as f:\n",
" data = f.read()\n",
" main = json.loads(data)\n",
" app = []\n",
" for cell in main['cells']:\n",
" if '## Run' in cell['source']:\n",
" break\n",
" if '# ignore\\n' in cell['source']:\n",
" continue\n",
" app.append(''.join(cell['source']))\n",
"\n",
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
" f.write('\\n\\n'.join(app))\n",
"\n",
"build()"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "fef1918f",
@ -277,17 +284,6 @@
"### tg build"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4489de4b",
"metadata": {},
"outputs": [],
"source": [
"!docker tag mongo:latest registry.spiritofweb.ru/mongo:latest"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
@ -300,8 +296,7 @@
"outputs": [],
"source": [
"!docker build -t $REGISTRY_HOST/api-tg:latest tg/."
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -315,8 +310,7 @@
"outputs": [],
"source": [
"!docker push $REGISTRY_HOST/api-tg:latest"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -338,8 +332,7 @@
"outputs": [],
"source": [
"!docker build -t $REGISTRY_HOST/avtopost-api:latest ."
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -353,8 +346,7 @@
"outputs": [],
"source": [
"!docker image push $REGISTRY_HOST/avtopost-api:latest"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -368,8 +360,7 @@
"outputs": [],
"source": [
"!kubectl rollout restart deployments/kultmost-api -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -391,8 +382,7 @@
"outputs": [],
"source": [
"!kubectl rollout restart deployments/vk-celery -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -406,8 +396,7 @@
"outputs": [],
"source": [
"!kubectl rollout restart deployments/email-celery -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -432,8 +421,7 @@
"docker pull gitea/gitea:1.22\n",
"docker tag gitea/gitea:1.22 $REGISTRY_HOST/gitea/gitea:1.22\n",
"docker push $REGISTRY_HOST/gitea/gitea:1.22"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -457,8 +445,7 @@
"docker tag busybox:1.36 $REGISTRY_HOST/busybox:1.36\n",
"docker tag rabbitmq:3.11-management $REGISTRY_HOST/rabbitmq:3.11-management\n",
"docker tag alpine:3.14 $REGISTRY_HOST/alpine:3.14"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -477,8 +464,7 @@
"docker push $REGISTRY_HOST/busybox:1.36\n",
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
"docker push $REGISTRY_HOST/alpine:3.14"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -500,8 +486,7 @@
"outputs": [],
"source": [
"!docker build -t $REGISTRY_HOST/ssh-tunnel:2 tunnel/"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -515,8 +500,7 @@
"outputs": [],
"source": [
"!docker push $REGISTRY_HOST/ssh-tunnel:2"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -530,8 +514,7 @@
"outputs": [],
"source": [
"!kubectl create secret generic ssh-id-rsa --from-file=id_rsa=/home/user/.ssh/id_rsa -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -545,8 +528,7 @@
"outputs": [],
"source": [
"!kubectl apply -n kultmost -f k8s/stunnel.yaml"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -568,8 +550,7 @@
"outputs": [],
"source": [
"!envsubst < k8s/prod.yaml | kubectl apply -n kultmost -f -"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -583,8 +564,7 @@
"outputs": [],
"source": [
"!envsubst < k8s/kultmost.yaml | kubectl apply -n kultmost -f -"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -598,8 +578,7 @@
"outputs": [],
"source": [
"!envsubst < k8s/mq.yaml | kubectl apply -n kultmost -f -"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -613,8 +592,7 @@
"outputs": [],
"source": [
"!envsubst < k8s/gitea.yaml | kubectl apply -n gitea -f -"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -628,8 +606,7 @@
"outputs": [],
"source": [
"!kubectl get pod -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -654,8 +631,7 @@
"echo \"$INIT_SSH'bash -s' <<'SCRIPT'\"\n",
"echo \"kubectl create secret docker-registry regcred --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\\\"$(git config --global --get user.email)\\\" && clear && history -c && history -w\"\n",
"echo \"SCRIPT\""
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -678,8 +654,15 @@
"echo \"clear\"\n",
"echo \"history -c\"\n",
"echo \"history -w\""
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
"id": "bbb93664",
"metadata": {},
"source": [
"## RSYNC"
]
},
{
"cell_type": "markdown",
@ -701,8 +684,7 @@
"outputs": [],
"source": [
"!rsync -avz -ogp --chown=www-data:www-data ./static/ kultmost:/var/www/static/"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -716,8 +698,7 @@
"outputs": [],
"source": [
"!echo $(git branch --show-current)"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -731,8 +712,7 @@
"outputs": [],
"source": [
"!rsync -avh --exclude-from=.rsyncignore --exclude=.venv/ . kultmost:/app/$(git branch --show-current)/"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -770,8 +750,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now production.service && systemctl status --no-pager -l production.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -801,8 +780,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -840,8 +818,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -877,8 +854,7 @@
"[Install]\n",
"WantedBy=default.target\n",
"EOF"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -908,8 +884,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -931,8 +906,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl restart production.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -946,8 +920,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl status production.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -961,8 +934,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl restart fastapi.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -976,8 +948,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl status fastapi.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -1015,8 +986,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now vk-celery.service && systemctl status --no-pager -l vk-celery.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1030,8 +1000,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl restart vk-celery.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1045,8 +1014,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl status vk-celery.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1076,8 +1044,7 @@
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now email-celery.service && systemctl status --no-pager -l email-celery.service'\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1091,8 +1058,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl restart email-celery.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1106,8 +1072,7 @@
"outputs": [],
"source": [
"!ssh kultmost 'systemctl status email-celery.service'"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -1129,8 +1094,7 @@
"outputs": [],
"source": [
"# !scp kultmost:/etc/nginx/sites-enabled/default nginx_conf "
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1146,8 +1110,7 @@
"!scp nginx_conf kultmost:/tmp/default\n",
"!ssh kultmost 'sudo mv /tmp/default /etc/nginx/sites-enabled/default'\n",
"!ssh kultmost 'nginx -t'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1161,15 +1124,14 @@
"outputs": [],
"source": [
"!ssh kultmost 'nginx -s reload'"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
"id": "a26138d2",
"metadata": {},
"source": [
"### microk8s"
"## microk8s"
]
},
{
@ -1187,8 +1149,7 @@
"sudo usermod -a -G microk8s $USER\n",
"newgrp microk8s\n",
"microk8s status"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1204,15 +1165,14 @@
"curl -LO https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg\n",
"helm install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.20.2 --namespace cert-manager --create-namespace --verify --keyring ./cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg --set crds.enabled=true\n",
"microk8s helm install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.20.2 --namespace cert-manager --create-namespace --verify --keyring ./cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg --set crds.enabled=true"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
"id": "622f00a3",
"metadata": {},
"source": [
"### Figma"
"## Figma"
]
},
{
@ -1426,13 +1386,12 @@
"\n",
"sss = await element_create(pc, tablet, mob)\n",
"sss.innerHTML"
],
"display_name": ".venv"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv (3.14.6.final.0)",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
@ -1446,7 +1405,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

View File

@ -257,7 +257,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

View File

@ -1,3 +1,4 @@
# `email_task.py` generate from `email_task.ipynb` by `build.ipynb`
#ignore
import os

View File

@ -70,96 +70,87 @@ spec:
targetPort: 22
nodePort: 30022
type: LoadBalancer
# ---
# kind: Ingress
# apiVersion: networking.k8s.io/v1
# metadata:
# name: gitea
# namespace: gitea
# annotations:
# cert-manager.io/cluster-issuer: google-public-ca
# nginx.ingress.kubernetes.io/proxy-body-size: "0"
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# spec:
# ingressClassName: public
# tls:
# - hosts:
# - git.kultmost.spiritofweb.ru
# secretName: git-tls-cert
# rules:
# - host: git.kultmost.spiritofweb.ru
# http:
# paths:
# - path: /
# pathType: Prefix
# backend:
# service:
# name: gitea-ssh
# port:
# number: 80
# - path: /robots.txt
# pathType: Exact
# backend:
# service:
# name: robots-static-svc
# port:
# number: 80
# ---
# apiVersion: v1
# kind: ConfigMap
# metadata:
# name: gitearobotscm
# namespace: gitea
# data:
# robots.txt: |
# User-agent: *
# Disallow: /
# ---
# apiVersion: apps/v1
# kind: Deployment
# metadata:
# name: robots-static
# namespace: gitea
# spec:
# replicas: 1
# selector:
# matchLabels:
# app: robots-static
# template:
# metadata:
# labels:
# app: robots-static
# spec:
# containers:
# - name: nginx
# image: nginx:alpine
# ports:
# - containerPort: 80
# volumeMounts:
# - name: robots
# mountPath: /usr/share/nginx/html
# volumes:
# - name: robots
# configMap:
# name: gitearobotscm
# ---
# apiVersion: v1
# kind: Service
# metadata:
# name: robots-static-svc
# namespace: gitea
# spec:
# selector:
# app: robots-static
# ports:
# - port: 80
# targetPort: 80
# ---
# apiVersion: v1
# kind: Secret
# metadata:
# name: ssh-creds
# type: Opaque
# stringData:
# username: "root"
# password: "$ROOT_PASS"
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: gitea
namespace: gitea
annotations:
cert-manager.io/cluster-issuer: google-public-ca
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: public
tls:
- hosts:
- git.kultmost.spiritofweb.ru
secretName: git-tls-cert
rules:
- host: git.kultmost.spiritofweb.ru
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-ssh
port:
number: 80
- path: /robots.txt
pathType: Exact
backend:
service:
name: robots-static-svc
port:
number: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gitearobotscm
namespace: gitea
data:
robots.txt: |
User-agent: *
Disallow: /
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: robots-static
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: robots-static
template:
metadata:
labels:
app: robots-static
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: robots
mountPath: /usr/share/nginx/html
volumes:
- name: robots
configMap:
name: gitearobotscm
---
apiVersion: v1
kind: Service
metadata:
name: robots-static-svc
namespace: gitea
spec:
selector:
app: robots-static
ports:
- port: 80
targetPort: 80

View File

@ -234,7 +234,7 @@
" # save to buffer as JPEG\n",
" buf = BytesIO()\n",
" # img.save(buf, format='JPEG', quality=85)\n",
" img.save(buf, format=\"WEBP\", quality=quality, method=6)\n",
" img.save(buf, format=\"WEBP\", quality=85, method=6)\n",
" buf.seek(0)\n",
"\n",
" # upload processed avatar to GridFS\n",
@ -1377,17 +1377,6 @@
"@app.get(\"/api/me\", response_model=dict)\n",
"async def get_me(response: Response, current_user: Dict = Security(get_current_user, scopes=[\"read:self\"])):\n",
" user = display_user(current_user)\n",
" refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)\n",
" if current_user['email'] == 'kultmost@yandex.ru':\n",
" response.set_cookie(\n",
" domain='.kultmost.ru',\n",
" key=\"token\",\n",
" value='2124|bhcbiP7XEgSe6ooTAoPf6mklBWNExG6a5CmmwTvTfafc5d5b',\n",
" httponly=True,\n",
" secure=PROD,\n",
" samesite=\"strict\",\n",
" max_age=int(refresh_exp.total_seconds())\n",
" )\n",
" return bson_clear(user)\n"
],
"display_name": ".venv"
@ -2090,7 +2079,6 @@
" print(data)\n",
" competition = await competitions_col.find_one({'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')})\n",
" if competition:\n",
" if PROD:\n",
" await competition_request_col.update_many(\n",
" {'user._id': data.user_id, 'competition_id': data.competition_id},\n",
" {'$set': {'status': True,\"posts.$[].status\": 'Проходит' }},\n",
@ -2099,14 +2087,23 @@
" social_vk = await social_collection.find_one({'user_id':current_user.get('_id'), 'group': {\"$exists\": False}, 'type':'vk'})\n",
" user = display_user(current_user)\n",
" user['social_vk'] = social_vk\n",
" await invitations_collection.insert_one({\n",
" await invitations_collection.update_one({\n",
" \"user_id\": data.user_id,\n",
" \"item\": competition['title'],\n",
" \"item\": competition.get('title'),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": user,\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" })\n",
" },{'$set':{\n",
" \"user_id\": data.user_id,\n",
" \"item\": competition.get('title'),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": user,\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" }},upsert=True)\n",
" return {'ok': True}\n",
" else:\n",
" return {'ok': False}\n"
@ -4173,9 +4170,32 @@
" user: Dict = Security(get_current_user)\n",
"):\n",
" data = await invitations_collection.find_one({'_id': ObjectId(invitation_id)})\n",
" match_ = {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'prod': PROD,\n",
" }\n",
" if data.get('competition'):\n",
" match_.update({'competition_id': data.get('competition').get('_id')})\n",
" if data.get('exhibition_id'):\n",
" match_.update({'exhibition_id': data.get('exhibition_id')})\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": match_},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" posts = [doc async for doc in cursor]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/invitation.html\", context={ \"user\": user, \"doc\": data}\n",
" request=request, name=\"profile/invitation.html\", context={ \"user\": user, \"doc\": data, \"data\": posts}\n",
" )"
],
"display_name": ".venv"
@ -4528,6 +4548,86 @@
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "6157e497",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/profile/delivery/competition/{competition_id}')\n",
"async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'competition_id': ObjectId(competition_id),\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery_old.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"data\": data,\n",
" }\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "d68edb20",
"metadata": {},
"outputs": [],
"source": [
"@app.get('/profile/delivery/exhibition/{exhibition_id}')\n",
"async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, user: Dict = Security(get_current_user)):\n",
" cursor = delivery_collection.aggregate([\n",
" {\"$match\": {\n",
" \"$or\": [\n",
" {\"sender._id\": user.get('_id')},\n",
" {\"user._id\": user.get('_id')},\n",
" {\"receiver._id\": user.get('_id')}\n",
" ],\n",
" 'exhibition_id': exhibition_id,\n",
" 'prod': PROD,\n",
" }},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" {\"$group\": {\n",
" \"_id\": \"$post_id\",\n",
" \"doc\": {\"$first\": \"$$ROOT\"}\n",
" }},\n",
" {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
" {\"$sort\": {\"_id\": -1}},\n",
" ])\n",
"\n",
" data = [doc async for doc in cursor]\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery_old.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"data\": data,\n",
" }\n",
" )\n"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
@ -4585,18 +4685,20 @@
"\n",
" data = [doc async for doc in cursor]\n",
" exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list()\n",
"\n",
" competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
" name=\"profile/delivery.html\",\n",
" context={\n",
" \"user\": user,\n",
" \"exhibitions\": exhibitions,\n",
" \"competitions\": competitions,\n",
" \"data\": data,\n",
" \"now\": datetime.now(),\n",
" \"page_title\": \"Доставка\",\n",
" \"active_section\": \"delivery\"\n",
" }\n",
" )"
" )\n"
],
"display_name": ".venv"
},
@ -4662,7 +4764,6 @@
"metadata": {},
"outputs": [],
"source": [
"\n",
"@app.get(\"/profile/partner/exhibition/{exhibition_id}/catalog\", response_class=HTMLResponse)\n",
"async def profile_partner_exhibitions_catalog(\n",
" request: Request,\n",
@ -4727,7 +4828,7 @@
" posts = [doc async for doc in posts_collection.find().sort('_id', -1).limit(32)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibition catalog.html\", context={\"now\": datetime.now(), \"user\": user, \"galerists\": galerists, \"genres\": genres, \"living_places\": living_places, \"exhibition\": exhibition, \"posts\": posts}\n",
" )"
" )\n"
],
"display_name": ".venv"
},
@ -4750,8 +4851,8 @@
" d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()\n",
" for post in posts:\n",
" for doc in d_posts:\n",
" if str(post.get('_id')) == doc.get('post_id'):\n",
" post.update({'status':doc.get('status'), 'd_id':doc.get('_id')})\n",
" if str(post.get('_id')) == doc.get('post_id') and doc.get('receiver') and doc.get('receiver').get('_id') == str(user.get('_id')):\n",
" post.update({'status':doc.get('status'), 'receiver': doc.get('receiver'), 'd_id':doc.get('_id')})\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibition.html\", context={\"now\": datetime.now(), \"user\": user, \"exhibition\": exhibition, \"posts\": posts}\n",
" )\n",
@ -4765,7 +4866,7 @@
" data = await exhibition_collection.find({'user._id': user.get('_id')}).sort('_id', -1).to_list()\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"profile/partner/exhibitions.html\", context={\"now\": datetime.now(), \"user\": user, \"data\": data}\n",
" )"
" )\n"
],
"display_name": ".venv"
},
@ -4887,7 +4988,7 @@
],
"metadata": {
"kernelspec": {
"display_name": ".venv (3.14.6.final.0)",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
@ -4901,7 +5002,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

130
main.py
View File

@ -1,3 +1,4 @@
# `main.py` generate from `main.ipynb` by `build.ipynb`
# FastAPI
## App
@ -126,7 +127,7 @@ async def logo_user_task(user: dict):
# save to buffer as JPEG
buf = BytesIO()
# img.save(buf, format='JPEG', quality=85)
img.save(buf, format="WEBP", quality=quality, method=6)
img.save(buf, format="WEBP", quality=85, method=6)
buf.seek(0)
# upload processed avatar to GridFS
@ -1069,17 +1070,6 @@ async def logout(
@app.get("/api/me", response_model=dict)
async def get_me(response: Response, current_user: Dict = Security(get_current_user, scopes=["read:self"])):
user = display_user(current_user)
refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
if current_user['email'] == 'kultmost@yandex.ru':
response.set_cookie(
domain='.kultmost.ru',
key="token",
value='2124|bhcbiP7XEgSe6ooTAoPf6mklBWNExG6a5CmmwTvTfafc5d5b',
httponly=True,
secure=PROD,
samesite="strict",
max_age=int(refresh_exp.total_seconds())
)
return bson_clear(user)
@ -1556,7 +1546,6 @@ async def partner_competition_request_artist(
print(data)
competition = await competitions_col.find_one({'_id':ObjectId(data.competition_id), 'user._id': current_user.get('_id')})
if competition:
if PROD:
await competition_request_col.update_many(
{'user._id': data.user_id, 'competition_id': data.competition_id},
{'$set': {'status': True,"posts.$[].status": 'Проходит' }},
@ -1565,14 +1554,23 @@ async def partner_competition_request_artist(
social_vk = await social_collection.find_one({'user_id':current_user.get('_id'), 'group': {"$exists": False}, 'type':'vk'})
user = display_user(current_user)
user['social_vk'] = social_vk
await invitations_collection.insert_one({
await invitations_collection.update_one({
"user_id": data.user_id,
"item": competition['title'],
"item": competition.get('title'),
"title": "Вы прошли отбор",
"type_": competition.get("type_"),
"user": user,
"prod": PROD,
"type": "Конкурс"
})
},{'$set':{
"user_id": data.user_id,
"item": competition.get('title'),
"title": "Вы прошли отбор",
"type_": competition.get("type_"),
"user": user,
"prod": PROD,
"type": "Конкурс"
}},upsert=True)
return {'ok': True}
else:
return {'ok': False}
@ -3187,9 +3185,32 @@ async def profile_invitation(
user: Dict = Security(get_current_user)
):
data = await invitations_collection.find_one({'_id': ObjectId(invitation_id)})
match_ = {
"$or": [
{"sender._id": user.get('_id')},
{"user._id": user.get('_id')},
{"receiver._id": user.get('_id')}
],
'prod': PROD,
}
if data.get('competition'):
match_.update({'competition_id': data.get('competition').get('_id')})
if data.get('exhibition_id'):
match_.update({'exhibition_id': data.get('exhibition_id')})
cursor = delivery_collection.aggregate([
{"$match": match_},
{"$sort": {"_id": -1}},
{"$group": {
"_id": "$post_id",
"doc": {"$first": "$$ROOT"}
}},
{"$replaceRoot": {"newRoot": "$doc"}},
{"$sort": {"_id": -1}},
])
posts = [doc async for doc in cursor]
return templates.TemplateResponse(
request=request, name="profile/invitation.html", context={ "user": user, "doc": data}
request=request, name="profile/invitation.html", context={ "user": user, "doc": data, "data": posts}
)
@app.get("/profile/invitation", response_class=HTMLResponse)
@ -3438,6 +3459,70 @@ async def profile_delivery_history_post(request: Request, post_id:str, user: Dic
)
@app.get('/profile/delivery/competition/{competition_id}')
async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):
cursor = delivery_collection.aggregate([
{"$match": {
"$or": [
{"sender._id": user.get('_id')},
{"user._id": user.get('_id')},
{"receiver._id": user.get('_id')}
],
'competition_id': ObjectId(competition_id),
'prod': PROD,
}},
{"$sort": {"_id": -1}},
{"$group": {
"_id": "$post_id",
"doc": {"$first": "$$ROOT"}
}},
{"$replaceRoot": {"newRoot": "$doc"}},
{"$sort": {"_id": -1}},
])
data = [doc async for doc in cursor]
return templates.TemplateResponse(
request=request,
name="profile/delivery_old.html",
context={
"user": user,
"data": data,
}
)
@app.get('/profile/delivery/exhibition/{exhibition_id}')
async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, user: Dict = Security(get_current_user)):
cursor = delivery_collection.aggregate([
{"$match": {
"$or": [
{"sender._id": user.get('_id')},
{"user._id": user.get('_id')},
{"receiver._id": user.get('_id')}
],
'exhibition_id': exhibition_id,
'prod': PROD,
}},
{"$sort": {"_id": -1}},
{"$group": {
"_id": "$post_id",
"doc": {"$first": "$$ROOT"}
}},
{"$replaceRoot": {"newRoot": "$doc"}},
{"$sort": {"_id": -1}},
])
data = [doc async for doc in cursor]
return templates.TemplateResponse(
request=request,
name="profile/delivery_old.html",
context={
"user": user,
"data": data,
}
)
@app.get(
"/profile/delivery",
response_class=HTMLResponse,
@ -3488,19 +3573,22 @@ async def profile_delivery(
data = [doc async for doc in cursor]
exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list()
competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list()
return templates.TemplateResponse(
request=request,
name="profile/delivery.html",
context={
"user": user,
"exhibitions": exhibitions,
"competitions": competitions,
"data": data,
"now": datetime.now(),
"page_title": "Доставка",
"active_section": "delivery"
}
)
@app.post("/api/exhibition/send/invitations")
async def api_exhibition_send_invitations(
background_tasks: BackgroundTasks,
@ -3540,7 +3628,6 @@ async def api_exhibition_post_select(
return {"ok": True}
@app.get("/profile/partner/exhibition/{exhibition_id}/catalog", response_class=HTMLResponse)
async def profile_partner_exhibitions_catalog(
request: Request,
@ -3607,6 +3694,7 @@ async def profile_partner_exhibitions_catalog(
request=request, name="profile/partner/exhibition catalog.html", context={"now": datetime.now(), "user": user, "galerists": galerists, "genres": genres, "living_places": living_places, "exhibition": exhibition, "posts": posts}
)
@app.get("/profile/partner/exhibition/{exhibition_id}", response_class=HTMLResponse)
async def profile_partner_exhibitions(
request: Request,
@ -3619,8 +3707,8 @@ async def profile_partner_exhibitions(
d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()
for post in posts:
for doc in d_posts:
if str(post.get('_id')) == doc.get('post_id'):
post.update({'status':doc.get('status'), 'd_id':doc.get('_id')})
if str(post.get('_id')) == doc.get('post_id') and doc.get('receiver') and doc.get('receiver').get('_id') == str(user.get('_id')):
post.update({'status':doc.get('status'), 'receiver': doc.get('receiver'), 'd_id':doc.get('_id')})
return templates.TemplateResponse(
request=request, name="profile/partner/exhibition.html", context={"now": datetime.now(), "user": user, "exhibition": exhibition, "posts": posts}
)

View File

@ -38,8 +38,7 @@
" load_env('env')\n",
"except:\n",
" load_env('env_example')"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -49,8 +48,7 @@
"outputs": [],
"source": [
"from db import *"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -60,13 +58,12 @@
"outputs": [],
"source": [
"await posts_collection.update_many({'status':'publish'},{'$set':{'status':'В каталоге'}})"
],
"display_name": ".venv"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv (3.14.6.final.0)",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
@ -80,7 +77,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

View File

@ -810,7 +810,7 @@
" # Генерация login link\n",
" token = await token_create(\n",
" str(user['_id']), \n",
" url=f'/profile/delivery#main'\n",
" url=f'/profile/invitation#main'\n",
" )\n",
" login_link = f'{FULL_DOMAIN}/login/{token}'\n",
"\n",
@ -860,7 +860,7 @@
" user_id=str(user['_id']),\n",
" type_item='invitation',\n",
" title='Приглашение',\n",
" link=f'/profile/delivery',\n",
" link=f'/profile/invitation',\n",
" text=f'Вы прошли отбор на конкурсе \"{competition.get(\"title\")}\"'\n",
" ))\n",
" all_allowed = all(\n",
@ -871,7 +871,24 @@
"\n",
" # Email-уведомление\n",
" if user.get('email') and all_allowed and has_approved:\n",
" print([i['title'] for i in posts_sorted])\n",
" await invitations_collection.update_one({\n",
" \"user_id\": str(user['_id']),\n",
" \"item\": competition.get(\"title\"),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": display_user(competition_owner),\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" },{'$set':{\n",
" \"user_id\": str(user['_id']),\n",
" \"item\": competition.get(\"title\"),\n",
" \"title\": \"Вы прошли отбор\",\n",
" \"competition\": competition,\n",
" \"type_\": competition.get(\"type_\"),\n",
" \"user\": display_user(competition_owner),\n",
" \"prod\": PROD,\n",
" \"type\": \"Конкурс\"\n",
" }},upsert=True)\n",
" if PROD and user.get('email') and all_allowed and has_approved:\n",
" email_data = {\n",
" 'text': 'Статус заявки обновлен',\n",
@ -937,7 +954,7 @@
],
"metadata": {
"kernelspec": {
"display_name": ".venv (3.14.6.final.0)",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
@ -951,7 +968,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

View File

@ -1,3 +1,4 @@
# `tasks.py` generate from `tasks.ipynb` by `build.ipynb`
from config import *
from db import *
from typing import Dict, Any, List, Optional, Callable, Union
@ -630,7 +631,7 @@ async def update_competition_req(competition, user, posts):
# Генерация login link
token = await token_create(
str(user['_id']),
url=f'/profile/delivery#main'
url=f'/profile/invitation#main'
)
login_link = f'{FULL_DOMAIN}/login/{token}'
@ -680,7 +681,7 @@ async def update_competition_req(competition, user, posts):
user_id=str(user['_id']),
type_item='invitation',
title='Приглашение',
link=f'/profile/delivery',
link=f'/profile/invitation',
text=f'Вы прошли отбор на конкурсе "{competition.get("title")}"'
))
all_allowed = all(
@ -691,7 +692,24 @@ async def update_competition_req(competition, user, posts):
# Email-уведомление
if user.get('email') and all_allowed and has_approved:
print([i['title'] for i in posts_sorted])
await invitations_collection.update_one({
"user_id": str(user['_id']),
"item": competition.get("title"),
"title": "Вы прошли отбор",
"type_": competition.get("type_"),
"user": display_user(competition_owner),
"prod": PROD,
"type": "Конкурс"
},{'$set':{
"user_id": str(user['_id']),
"item": competition.get("title"),
"title": "Вы прошли отбор",
"competition": competition,
"type_": competition.get("type_"),
"user": display_user(competition_owner),
"prod": PROD,
"type": "Конкурс"
}},upsert=True)
if PROD and user.get('email') and all_allowed and has_approved:
email_data = {
'text': 'Статус заявки обновлен',

File diff suppressed because one or more lines are too long

View File

@ -166,7 +166,7 @@
<span class="offer-text">Публикация в личных соц. сетях</span>
</label>
-->
<button class="c_aq9et" id="createBtn">
<button class="c_aq9et ba" id="createBtn">
<span class="c_som9s">Сохранить изменения</span>
</button>
</div>
@ -645,7 +645,7 @@ document.addEventListener('DOMContentLoaded', () => {
{{ competition.style() }}
{{ checkbox.style() }}
<style>
.toggle-group {
.toggle-group {
display: flex;
margin-bottom: 15px;
border: 1px solid #ddd;

View File

@ -170,7 +170,7 @@
</label> -->
<button class="c_aq9et" id="createBtn" type="button">
<button class="c_aq9et ba" id="createBtn" type="button">
<span class="c_som9s">Создать конкурс</span>
</button>
</div>

View File

@ -57,17 +57,17 @@
</header>
</div>
<div style="grid-area: c;display: grid;gap: 10px;">
<a class="c_wcdb7 b_ofiw3" href="{% if competition.get('rules_link') %}{{competition.rules_link}}{% else %}/b/competition/{{ competition.rules_file_id }}{% endif %}" rel="noopener noreferrer">
<a class="c_wcdb7 b_ofiw3 ba" href="{% if competition.get('rules_link') %}{{competition.rules_link}}{% else %}/b/competition/{{ competition.rules_file_id }}{% endif %}" rel="noopener noreferrer">
<span class="b_j2ln2">Условия конкурса</span>
</a>
{% if competition.user._id == '681684390000000000000000'%}
<a class="c_wcdb7 b_ofiw3" href="https://vk.ru/im?sel=25126640" rel="noopener noreferrer">
<a class="c_wcdb7 b_ofiw3 ba" href="https://vk.ru/im?sel=25126640" rel="noopener noreferrer">
<span class="b_j2ln2">Задать вопрос</span>
</a>
{% endif %}
<!-- TODO -->
{% if competition.get('status',-1) == 0%}
<a class="c_wcdb7 b_ofiw3" href="/profile/competition/{{competition._id}}#main">
<a class="c_wcdb7 b_ofiw3 ba" href="/profile/competition/{{competition._id}}#main">
<span class="b_j2ln2">Отправить заявку на конкурс</span>
</a>
{% endif %}

View File

@ -37,7 +37,7 @@
</header>
</div>
{% if competition.get('status',-1) == 0%}
<a class="c_wcdb7 b_ofiw3" href="/profile/competition/{{competition._id}}#main"><span class="b_j2ln2">Отправить заявку на конкурс</span></a>
<a class="c_wcdb7 b_ofiw3 ba" href="/profile/competition/{{competition._id}}#main"><span class="b_j2ln2">Отправить заявку на конкурс</span></a>
{% endif %}
<a class="c_rpq63 b_lyhks" href="/competition/{{competition._id}}">
<div class="c_fqmiw b_ilrcq"><img class="c_3jjk6 b_cm8eh" src="/a/{{member._id}}" crossorigin="anonymous" fetchpriority="high" loading="eager">

View File

@ -2,11 +2,11 @@
{% if user %}
<div class="st">
{% if 'partner' in user.get('profile', '') %}
<a href="/form/competition">Добавить конкурс</a>
<a href="/form/exhibition">Добавить выставку</a>
<a class="ba" href="/form/competition">Добавить конкурс</a>
<a class="ba" href="/form/exhibition">Добавить выставку</a>
{% else %}
<a href="/form/post">Добавить пост</a>
<a href="/form/news">Добавить новость</a>
<a class="ba" href="/form/post">Добавить пост</a>
<a class="ba" href="/form/news">Добавить новость</a>
{%endif%}
</div>
{% endif %}
@ -21,33 +21,8 @@
{% macro style() -%}
<style>
.st {
position: sticky;
bottom: 0;
background: white;
width: 100%;
padding: 0 15px;
border-top: 1px solid #5F1798;
display: flex;
flex-direction: column;
z-index: 99;
display: flex;
align-items: center;
transition: none;
}
.st>a {
padding: 13px 15px;
width: 335px;
justify-content: center;
align-items: center;
border-radius: 5px;
background: #5F1798;
color: white;
display: flex;
margin: 4px 0;
font-size: 13px;
}
.st {position: sticky;bottom: 0;background: white;width: 100%;padding: 0 15px;border-top: 1px solid #5F1798;display: flex;flex-direction: column;z-index: 99;display: flex;align-items: center;transition: none;}
.st>a {padding: 13px 15px;width: 335px;justify-content: center;align-items: center;border-radius: 5px;background: #5F1798;color: white;display: flex;margin: 4px 0;font-size: 13px;}
@media (min-width: 901px) {
.c_62bje { color: #5F1798; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 700; line-height: normal; }
.c_jtxml { color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 700; line-height: normal; }

View File

@ -15,11 +15,11 @@
{% if 'admin' not in user.get('roles', []) %}
{% if 'partner' in user.get('roles', [])%}
{% if 'partner' in user.get('profile', '') %}
<button class="c_6cfr8 b_m2ah9 b" onclick="switchProfile()">
<button class="c_6cfr8 b_m2ah9 b ba" onclick="switchProfile()">
<span class="c_d2l0b b_al8e7">Личный кабинет галериста</span>
</button>
{% else %}
<button class="c_6cfr8 b_m2ah9 b" onclick="switchProfile()">
<button class="c_6cfr8 b_m2ah9 b ba" onclick="switchProfile()">
<span class="c_d2l0b b_al8e7">Личный кабинет партнера</span>
</button>
{% endif %}

View File

@ -112,6 +112,10 @@
{% macro style() -%}
<style>
.c_nnsh6 > a:hover > span {
transform: scale(1.1);
color: #5f1798;
}
.b_xsvl1 { color: #5F1798; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 700; line-height: normal; text-align: center; padding: 0 10px;}
.b_u9ode { align-items: center; align-self: stretch; background: #FFF; border-radius: 5px; display: flex; justify-content: center; padding: 10px 0; }
.b_al8e7 { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
@ -127,9 +131,9 @@
.b_v63fu { align-items: center; background: #F39318; border-radius: 5px; display: flex; justify-content: center; padding: 15px 55px; }
.b_h9dsc { display: none; align-items: center; align-self: stretch; background: #FFF; border-radius: 5px; flex-direction: column; gap: 15px; justify-content: center; padding: 10px 0; }
.b_ld219 { color: #171717; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 700; line-height: normal; }
.b_ny6vg { align-items: center; background: #FFF; box-shadow: 0 -1px 0 0 #5F1798 inset; display: flex; height: 35px; justify-content: center; padding: 0 16px; }
.b_ny6vg { align-items: center; box-shadow: 0 -1px 0 0 #5F1798 inset; display: flex; height: 35px; justify-content: center; padding: 0 16px; }
.b_v15gw { color: #969696; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 700; line-height: normal; }
.b_cros1 { align-items: center; background: #FFF; display: flex; height: 35px; justify-content: center; padding: 0 15px; }
.b_cros1 { align-items: center; display: flex; height: 35px; justify-content: center; padding: 0 15px; }
.b_289i1 { align-items: center; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 13px 55px; }
.b_3kfnv { align-self: stretch; height: 400px; }
.b_xcy5t { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }

View File

@ -161,7 +161,7 @@
{% endfor %}
</datalist>
<button class="c_aq9et" id="createBtn">
<button class="c_aq9et ba" id="createBtn">
<span class="c_som9s">Создать выставку</span>
</button>
</div>

View File

@ -128,7 +128,7 @@
<option value="{{ doc }}"></option>
{% endfor %}
</datalist>
<button class="c_aq9et" id="createBtn">
<button class="c_aq9et ba" id="createBtn">
<span class="c_som9s">Сохранить изменения</span>
</button>
</div>

View File

@ -39,7 +39,7 @@
<span class="login-hint">Минимум 8 символов</span>
<a href="/restore{% if next %}?next={{next}}{% endif %}" onclick="sendToNextPage(event)" class="login-forgot" style="margin-bottom: 1rem;">Забыли пароль ?</a>
<button class="c_aq9et" id="createBtn" type="button">
<button class="c_aq9et ba" id="createBtn" type="button">
<span class="c_som9s">Войти</span>
</button>
<div style="display: flex; gap:15px">
@ -482,12 +482,6 @@ input[type="number"]:focus::-webkit-inner-spin-button {display: none;}
/* убираем большой нижний отступ у группы, за которой идёт подсказка */
.floating-group:has(+ .login-hint) { margin-bottom: 0; }
/* Микроанимация кнопки «Войти» */
.c_aq9et { cursor: pointer; box-shadow: 0 4px 14px -4px rgba(95,23,152,.5); transition: transform .18s, box-shadow .18s, background .18s; }
.c_aq9et:hover { background: #6d1ba9; transform: translateY(-2px); box-shadow: 0 10px 24px -6px rgba(95,23,152,.6); }
.c_aq9et:active { transform: translateY(0); box-shadow: 0 4px 12px -4px rgba(95,23,152,.5); }
@media (prefers-reduced-motion: reduce) { .c_aq9et:hover, .c_aq9et:active { transform: none; } }
/* Картинка слева от формы (только десктоп) */
.login-illustration { display: none; }
@media (min-width: 901px) {

View File

@ -28,7 +28,7 @@
</script>
{% endif %}
<div class="c_rtopc">
<label class="c_aq9et" for="fileInput">
<label class="c_aq9et ba" for="fileInput">
<span class="c_som9s">Добавить фото</span>
</label>
<input class="c_1gai0" id="fileInput" type="file" multiple accept="image/*" style="display: none;"/>

View File

@ -27,7 +27,7 @@
</script>
{% endif %}
<div class="c_rtopc">
<label class="c_aq9et" for="fileInput">
<label class="c_aq9et ba" for="fileInput">
<span class="c_som9s">Добавить фото</span>
</label>
<input class="c_1gai0" id="fileInput" type="file" multiple accept="image/*" style="display: none;"/>

View File

@ -39,12 +39,12 @@
<textarea type="text" id="description" name="description" class="c_4ykpn"></textarea>
<label for="description" class="c_z3ybu floating-label">Описание</label>
</div>
<button class="c_aq9et" id="createBtn" type="button">
<button class="c_aq9et ba" id="createBtn" type="button">
<span class="c_som9s">Сохранить</span>
</button>
</div>
<div class="c_rtopc">
<label class="c_aq9et" for="fileInput">
<label class="c_aq9et ba" for="fileInput">
<span class="c_som9s">Добавить фото</span>
</label>
<input class="c_1gai0" id="fileInput" type="file" multiple accept="image/*" style="display: none;"/>
@ -328,8 +328,7 @@
{%- endmacro %}
{% macro style() -%}
<style>
<style>
.preview {
display: flex;
gap: 8px;

View File

@ -96,7 +96,7 @@
</div>
{% endfor %}
{% else %}
<a class="c_7d3qb b_dc4rx" type="button" href="/profile/social">
<a class="c_7d3qb b_dc4rx ba" type="button" href="/profile/social">
<span class="c_v3bsp b_xj1br">Добавить личную соцсеть</span>
</a>
{% endif %}
@ -120,7 +120,7 @@
</div>
</div>
</div>
<button class="c_aq9et" id="createBtn" type="button">
<button class="c_aq9et ba" id="createBtn" type="button">
<span class="c_som9s">Опубликовать</span>
</button>
</div>
@ -247,7 +247,7 @@
})
</script>
<div class="c_rtopc">
<label class="c_aq9et" for="fileInput">
<label class="c_aq9et ba" for="fileInput">
<span class="c_som9s">Добавить фото</span>
</label>
<input class="c_1gai0" id="fileInput" type="file" multiple accept="image/*" style="display: none;"/>
@ -521,7 +521,7 @@
{% block head %}
{{ header.style() }}
<style>/* Base (common) styles */
<style>
.b_xj1br { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.b_dc4rx { align-items: center; align-self: stretch; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 12px 0; }
.b_5rck0 { align-items: center; align-self: stretch; background: #F39318; border-radius: 5px; display: flex; justify-content: center; padding: 12px 0; }

View File

@ -12,38 +12,42 @@
{% block content %}
{{ header.section(user) }}
<section class="c_dg3k4">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'competitions') }}
<div class="c_3ihvi" id="main"><h2>{{competition.title}}</h2><p>{{competition.topic}}</p><p>Минимальное кол-во картин: {{competition.min_paintings}}. Максимальное кол-во картин: {{competition.max_paintings}}</p>
<a class="c_ctfod b_289i1" href="{% if competition.get('rules_link') %}{{competition.rules_link}}{% else %}/b/competition/{{ competition.rules_file_id }}{% endif %}" rel="noopener noreferrer"><span class="c_d2l0b b_al8e7">Условия конкурса</span></a>
<a class="c_ctfod b_289i1" href="/form/post?next=/profile/competition/{{ competition._id }}"><span class="c_d2l0b b_al8e7">Загрузить картину в список</span></a>
</div>
<div class="c_3ihvi"><h3>Выбрано картин: <span id="selected_span">0</span></h3>
</div>
<div class="c_3ihvi">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'competitions') }}
<div class="c_3ihvi" id="main">
<h2>{{competition.title}}</h2>
<p>{{competition.topic}}</p>
<p>Минимальное кол-во картин: {{competition.min_paintings}}. Максимальное кол-во картин: {{competition.max_paintings}}</p>
<a class="c_ctfod b_289i1 ba" href="{% if competition.get('rules_link') %}{{competition.rules_link}}{% else %}/b/competition/{{ competition.rules_file_id }}{% endif %}" rel="noopener noreferrer"><span class="c_d2l0b b_al8e7">Условия конкурса</span></a>
<a class="c_ctfod b_289i1 ba" href="/form/post?next=/profile/competition/{{ competition._id }}"><span class="c_d2l0b b_al8e7">Загрузить картину в список</span></a>
</div>
<div class="c_3ihvi">
<h3>Выбрано картин: <span id="selected_span">0</span></h3>
</div>
<div class="c_3ihvi">
<label class="offer-label">
<input type="checkbox" id="acceptOffer" class="offer-checkbox">
<span class="offer-text">Принять условия конкурса</span>
</label>
{% if competition.user._id == '681684390000000000000000' %}
<a class="c_ctfod b_289i1" href="https://vk.ru/im?sel=25126640" rel="noopener noreferrer">
<a class="c_ctfod b_289i1 ba" href="https://vk.ru/im?sel=25126640" rel="noopener noreferrer">
<span class="c_d2l0b b_al8e7">Задать вопрос в VK</span>
</a>
{% endif %}
<button class="c_ctfod b_289i1" id="sendForm">
<button class="c_ctfod b_289i1 ba" id="sendForm">
<span class="c_d2l0b b_al8e7">Отправить заявку</span>
</button>
</div>
<div class="c_3ihvi">
</div>
<div class="c_3ihvi">
{% if posts|length > 5 %}
<div class="inptS">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.6 21L13.3 14.7C12.8 15.1 12.225 15.4167 11.575 15.65C10.925 15.8833 10.2333 16 9.5 16C7.68333 16 6.14583 15.3708 4.8875 14.1125C3.62917 12.8542 3 11.3167 3 9.5C3 7.68333 3.62917 6.14583 4.8875 4.8875C6.14583 3.62917 7.68333 3 9.5 3C11.3167 3 12.8542 3.62917 14.1125 4.8875C15.3708 6.14583 16 7.68333 16 9.5C16 10.2333 15.8833 10.925 15.65 11.575C15.4167 12.225 15.1 12.8 14.7 13.3L21 19.6L19.6 21ZM9.5 14C10.75 14 11.8125 13.5625 12.6875 12.6875C13.5625 11.8125 14 10.75 14 9.5C14 8.25 13.5625 7.1875 12.6875 6.3125C11.8125 5.4375 10.75 5 9.5 5C8.25 5 7.1875 5.4375 6.3125 6.3125C5.4375 7.1875 5 8.25 5 9.5C5 10.75 5.4375 11.8125 6.3125 12.6875C7.1875 13.5625 8.25 14 9.5 14Z" fill="#1F1F1F"></path>
</svg>
<path
d="M19.6 21L13.3 14.7C12.8 15.1 12.225 15.4167 11.575 15.65C10.925 15.8833 10.2333 16 9.5 16C7.68333 16 6.14583 15.3708 4.8875 14.1125C3.62917 12.8542 3 11.3167 3 9.5C3 7.68333 3.62917 6.14583 4.8875 4.8875C6.14583 3.62917 7.68333 3 9.5 3C11.3167 3 12.8542 3.62917 14.1125 4.8875C15.3708 6.14583 16 7.68333 16 9.5C16 10.2333 15.8833 10.925 15.65 11.575C15.4167 12.225 15.1 12.8 14.7 13.3L21 19.6L19.6 21ZM9.5 14C10.75 14 11.8125 13.5625 12.6875 12.6875C13.5625 11.8125 14 10.75 14 9.5C14 8.25 13.5625 7.1875 12.6875 6.3125C11.8125 5.4375 10.75 5 9.5 5C8.25 5 7.1875 5.4375 6.3125 6.3125C5.4375 7.1875 5 8.25 5 9.5C5 10.75 5.4375 11.8125 6.3125 12.6875C7.1875 13.5625 8.25 14 9.5 14Z"
fill="#1F1F1F"></path>
</svg>
<input id="memberSearch" list="membersList" type="search" placeholder="Поиск ..." aria-label="Поиск" />
<datalist id="membersList">
{% for post in posts %}
@ -51,17 +55,13 @@
{% endfor %}
</datalist>
</div>
{% endif %}
{% endif %}
</div>
<div class="c_tihtk">
<div class="c_tihtk">
{% for post in posts %}
{{ card.section(post, competition.get('nominations',[])) }}
{% endfor %}
</div>
</div>
<script>
(function(){
const input = document.getElementById('memberSearch');

View File

@ -4,6 +4,7 @@
{% import "components/profile_card.html" as profile_card %}
{% import "components/profile_nav.html" as profile_nav %}
{% import "competition/list.html" as competition %}
{% import "competition/card.html" as card %}
{% block title %}Где картины{% endblock %}
@ -73,12 +74,12 @@
</div>
{% endif %}
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
<button class="b_wy3rt" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
<span class="b_bvl0d">Отправить</span>
</button>
{% endif %}
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
<button class="b_wy3rt" onclick="btnAccept(this, '{{doc._id}}')">
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
<span class="b_bvl0d">Получить</span>
</button>
{% endif %}
@ -100,223 +101,99 @@
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'delivery') }}
<div class="c_tihtk" id="main">
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
<article class="c_9fgdh b_ax8vz">
{% for competition in competitions %}
<article class="c_9fgdh b_ax8vz" id="l{{competition._id}}">
<a href="/profile/delivery/competition/{{competition._id}}#main">
<div class="b_ornr9">
<picture>
<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">
<a href="/profile/delivery/competition/{{competition._id}}#main" class="b_73p10">
<div class="b_vabj9_2">
<span class="b_0jst1_2">Конкурс</span>
</div>
{% if competition.get('status',-1) == 2 %}
<div class="b_vabj9_2">
<span class="b_0jst1_2">Закончился</span>
</div>
{% elif competition.get('status',-1) == 1 %}
<div class="b_vabj9_2">
<span class="b_0jst1_2">Идет отбор</span>
</div>
{% else %}
<div class="b_vabj9">
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
</div>
{% endif %}
<header class="c_m876y b_9xtz9">
<h3 class="b_8f3uh">Фильтры</h3>
<h3 class="c_hysjg b_8f3uh" style="color: #222;">{{ competition.title }}</h3>
</header>
<div class="c_aihm6 floating-group">
<select class="c_xhd5k" id="exhibition_id" name="exhibition_id" required
onchange="filterFigure(this.value)">
<option value="">Выбрать выставку</option>
{% for doc in exhibitions %}
<option value="{{doc._id}}">{{doc.title}}</option>
{% endfor %}
</select>
<label for="exhibition_id" class="c_z3ybu floating-label">Выставка</label>
</div>
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
<header class="c_m876y b_9xtz9">
<h3 class="b_8f3uh">Настройки отправки</h3>
</header>
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
<div class="b_73p10">
<div class="c_aihm6 floating-group">
<select class="c_xhd5k" type="text" id="delivery_type" name="delivery_type" required>
<option>СДЭК</option>
<option>Самостоятельно</option>
</select>
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
</div>
<div class="c_aihm6 floating-group">
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
data-toast="Поле Трэк-номер должно быть заполнено">
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
</div>
<p class="c_2fr39 b_tfnz2">{{ competition.topic }}</p>
{% if profile %}
<button class="c_aq9et">
<span class="c_som9s">Посмотреть заявки</span>
</button>
{% else %}
{% set user = competition.get('user') %}
<div class="c_dbbe4 b_otttb">
{% if user %}
<img class="c_pm80w b_a68fq" src="/a/{{user._id}}" alt="{{ user.username | e }}" crossorigin="anonymous" fetchpriority="high" loading="eager">
<div class="c_4402p b_ntazj">
<p class="c_kvah4 b_ghosy">{{ user.username | e }}</p>
</div>
{% endif %}
</div>
<p class="c_2fr39 b_tfnz2">Подробнее</p>
{% endif %}
</a>
<p class="c_drpqj b_mp1wb">{{ (competition.datetime_start.strftime('%d.%m.%Y')) if competition.datetime_start else ''}}</p>
</div>
</article>
{% for doc in data if doc.status in statuses %}
{{ render_doc(doc) }}
{% endfor %}
{% for doc in data if doc.status not in statuses %}
{{ render_doc(doc) }}
{% endfor %}
<script defer>
const query = window.location.href.split('?')[1] || '';
let exhibition_id = null;
if (query) {
const parts = query.split('=');
if (parts[0] === 'exhibition_id' && parts[1] !== 'None') {
exhibition_id = parts[1];
}
}
function filterFigure(exhibition_id) {
document.getElementById('main').querySelectorAll('figure').forEach(figure => {
if (!exhibition_id || figure.dataset.exhibition_id === exhibition_id) {
figure.style.display = 'flex';
console.log(figure.dataset.exhibition_id, exhibition_id, 'flex')
} else {
console.log(figure.dataset.exhibition_id, exhibition_id)
figure.style.display = 'none';
}
});
}
if (exhibition_id) {
document.getElementById('exhibition_id').value = exhibition_id;
}
filterFigure(exhibition_id);
</script>
<script>
const deliveryType = document.getElementById('delivery_type');
const track = document.getElementById('track');
function toggleTrack() {
const isSelf = deliveryType.value === 'Самостоятельно';
if (isSelf) {
track.required = false;
track.value = '';
} else {
track.required = true;
}
}
deliveryType.addEventListener('change', toggleTrack);
toggleTrack();
</script>
<script>
async function btnAccept(btn, itemId){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
try {
const res = await fetch('/api/delivery/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: itemId,
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1';
statusSpan.className = 'b_5dr47';
statusSpan.textContent = 'Получено';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
async function btnSend(btn, itemId, sender, too){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
const delivery_type = document.getElementById('delivery_type').value;
const trackValue = document.getElementById('track');
let track = ''
if (delivery_type === 'СДЭК' && trackValue) {
track = trackValue.value
}
const requiredInputs = document.querySelectorAll('input[required]');
const requiredTextareas = document.querySelectorAll('textarea[required]');
const requiredFields = [...requiredInputs, ...requiredTextareas];
let firstErrorField = null;
let hasError = false;
requiredFields.forEach(field => {
const group = field.closest('.floating-group');
if (!group) return; // skip if no group wrapper
let isEmpty = false;
if (field.type === 'file') {
isEmpty = !field.files || field.files.length === 0;
} else {
isEmpty = !field.value || !field.value.trim();
}
if (isEmpty) {
group.classList.add('error');
hasError = true;
showToast({message: field.dataset.toast, duration:2500});
if (!firstErrorField) firstErrorField = field;
} else {
group.classList.remove('error');
}
});
if (hasError && firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstErrorField.focus(), 600);
return;
}
try {
const res = await fetch('/api/delivery/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sender,
too,
item: itemId,
delivery_type,
track: track
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
// If response isn't JSON, try to get text
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
// Fallback to generic error
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1_2';
statusSpan.className = 'b_5dr47_2';
statusSpan.textContent = 'Ожидает получения';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
</script>
{% endfor %}
{% for i in exhibitions %}
<article class="c_9fgdh b_ax8vz">
<a href="/profile/delivery/exhibition/{{i._id}}#main">
<div class="b_ornr9">
<picture>
<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">
</picture>
</div>
</a>
<div class="c_z3dpx b_73p10">
<a class="b_73p10" href="/profile/delivery/exhibition/{{i._id}}#main">
<div class="b_vabj9_2">
<span class="b_0jst1_2">Выставка</span>
</div>
<div class="c_c4wam b_vabj9">
{% if now > i.date_finish %}
<span class="c_95csi b_0jst1">Закончилась</span>
{% elif now > i.datetime_start %}
<span class="c_95csi b_0jst1">Началась</span>
{% else %}
<span class="c_95csi b_0jst1">Формируется</span>
{% endif %}
</div>
<header class="c_m876y b_9xtz9">
<h3 class="c_hysjg b_8f3uh" id="preview_title" style="color: #222;">{{i.title}}</h3>
</header>
<p class="c_2fr39 b_tfnz2" id="preview_topic">{{i.topic}}</p>
<div class="c_dbbe4 b_otttb">
{% if user %}
<img crossorigin="anonymous" fetchpriority="high" loading="eager" class="c_pm80w b_a68fq" src="/a/{{i.user._id}}" alt="{{ i.user.username | e }}">
<div class="c_4402p b_ntazj">
<p class="c_kvah4 b_ghosy">{{ i.user.username }}</p>
</div>
{% 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>
</div>
</div>
@ -329,7 +206,9 @@ async function btnSend(btn, itemId, sender, too){
{{ header.style() }}
{{ competition.style() }}
{{ profile_nav.style() }}
<style>/* Base (common) styles */
{{card.style()}}
<style>
/* Base (common) styles */
.c_aihm6{width: 100%;}
.mw{margin: 0 20px;
width: 100%;}
@ -439,7 +318,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.fl{display: flex;justify-content: space-between;width: 100%;}
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.c_aq9et { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
.b_ornr9 { align-self: stretch; height: 400px; overflow: hidden;}
.b_ornr9 { align-self: stretch; overflow: hidden;}
.b_0jst1 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_vabj9 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_0jst1_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }

View File

@ -0,0 +1,564 @@
{% extends "base.html" %}
{% import "/components/header.html" as header %}
{% import "/components/footer.html" as footer %}
{% import "components/profile_card.html" as profile_card %}
{% import "components/profile_nav.html" as profile_nav %}
{% import "competition/list.html" as competition %}
{% block title %}Где картины{% endblock %}
{% macro render_doc(doc) %}
<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}}">
<figcaption class="c_3g8gh b_pvw5a" data-id="{{doc._id}}">
{% if doc.status == 'Ожидает отправки' %}
<div class="b_thlt1">
<span class="b_5dr47">{{doc.status}}</span>
</div>
{% elif doc.status == 'Получено' %}
<div class="b_thlt1">
<span class="b_5dr47">{{doc.status}}</span>
</div>
{% elif doc.status == 'Ожидает получения' %}
<div class="b_thlt1_2">
<span class="b_5dr47_2">{{doc.status}}</span>
</div>
{% else %}
<div class="b_thlt1_2">
<span class="b_5dr47_2">{{doc.status}}</span>
</div>
{% endif %}
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
<h2 class="c_9hvtd b_f5fb2">{{doc.title}}</h2>
{% if doc.type == 'exhibition' and doc.get('exhibition') %}
<p class="b_6spaf">{{doc.get('exhibition').title}}</p>
<div class="b_xz6em">
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Организатор</span>
<span class="c_zm0m4 b_09est">{{doc.receiver.username}}</span>
</div>
</div>
{% else %}
{% if doc.address %}
<p class="b_6spaf">Адрес: {{doc.address}}</p>
{% endif %}
{% if doc.back_address %}
<p class="b_6spaf">Адрес для возврата: {{doc.back_address}}</p>
{% endif %}
{% if doc.delivery_type %}
<p class="b_6spaf">Способ доставки: {{doc.delivery_type}}</p>
{% endif %}
{% if doc.track %}
<p class="b_6spaf">Трэк-номер: {{doc.track}}</p>
{% endif %}
{% if doc.sender %}
<div class="b_xz6em">
<img src="/a/{{doc.sender._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Отправитель</span>
<span class="c_zm0m4 b_09est">{{doc.sender.username}}</span>
</div>
</div>
{% endif %}
{% if doc.receiver %}
<div class="b_xz6em">
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Получатель</span>
<span class="c_zm0m4 b_09est">{{doc.receiver.username}}</span>
<span class="b_qla2q">+7 *** *** {{ doc.receiver.phone[-4:] }}</span>
</div>
</div>
{% endif %}
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
<span class="b_bvl0d">Отправить</span>
</button>
{% endif %}
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
<span class="b_bvl0d">Получить</span>
</button>
{% endif %}
{% endif %}
<p class="b_6spaf">
{{ (doc._id.generation_time.strftime('%d.%m.%Y')) if doc._id else ''}}
<b>{{ '%02d' % ((doc._id.generation_time.hour + 3) % 24) if doc._id else '' }}:{{ '%02d' % doc._id.generation_time.minute if doc._id else '' }}</b>
</p>
</figcaption>
</figure>
{% endmacro %}
{% block content %}
{{ header.section(user) }}
<section class="c_dg3k4">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'delivery') }}
<div class="c_tihtk" id="main">
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
<article class="c_9fgdh b_ax8vz">
<div class="c_z3dpx b_73p10">
<header class="c_m876y b_9xtz9">
<h3 class="b_8f3uh">Настройки отправки</h3>
</header>
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
<div class="b_73p10">
<div class="c_aihm6 floating-group">
<select class="c_xhd5k" type="text" id="delivery_type" name="delivery_type" required>
<option>СДЭК</option>
<option>Самостоятельно</option>
</select>
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
</div>
<div class="c_aihm6 floating-group">
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
data-toast="Поле Трэк-номер должно быть заполнено">
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
</div>
</div>
</div>
</article>
{% endif %}
{% for doc in data if doc.status in statuses %}
{{ render_doc(doc) }}
{% endfor %}
{% for doc in data if doc.status not in statuses %}
{{ render_doc(doc) }}
{% endfor %}
<script defer>
const query = window.location.href.split('?')[1] || '';
let exhibition_id = null;
if (query) {
const parts = query.split('=');
if (parts[0] === 'exhibition_id' && parts[1] !== 'None') {
exhibition_id = parts[1];
}
}
function filterFigure(exhibition_id) {
document.getElementById('main').querySelectorAll('figure').forEach(figure => {
if (!exhibition_id || figure.dataset.exhibition_id === exhibition_id) {
figure.style.display = 'flex';
console.log(figure.dataset.exhibition_id, exhibition_id, 'flex')
} else {
console.log(figure.dataset.exhibition_id, exhibition_id)
figure.style.display = 'none';
}
});
}
if (exhibition_id) {
document.getElementById('exhibition_id').value = exhibition_id;
}
filterFigure(exhibition_id);
</script>
<script>
const deliveryType = document.getElementById('delivery_type');
const track = document.getElementById('track');
function toggleTrack() {
const isSelf = deliveryType.value === 'Самостоятельно';
if (isSelf) {
track.required = false;
track.value = '';
} else {
track.required = true;
}
}
deliveryType.addEventListener('change', toggleTrack);
toggleTrack();
</script>
<script>
async function btnAccept(btn, itemId){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
try {
const res = await fetch('/api/delivery/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: itemId,
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1';
statusSpan.className = 'b_5dr47';
statusSpan.textContent = 'Получено';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
async function btnSend(btn, itemId, sender, too){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
const delivery_type = document.getElementById('delivery_type').value;
const trackValue = document.getElementById('track');
let track = ''
if (delivery_type === 'СДЭК' && trackValue) {
track = trackValue.value
}
const requiredInputs = document.querySelectorAll('input[required]');
const requiredTextareas = document.querySelectorAll('textarea[required]');
const requiredFields = [...requiredInputs, ...requiredTextareas];
let firstErrorField = null;
let hasError = false;
requiredFields.forEach(field => {
const group = field.closest('.floating-group');
if (!group) return; // skip if no group wrapper
let isEmpty = false;
if (field.type === 'file') {
isEmpty = !field.files || field.files.length === 0;
} else {
isEmpty = !field.value || !field.value.trim();
}
if (isEmpty) {
group.classList.add('error');
hasError = true;
showToast({message: field.dataset.toast, duration:2500});
if (!firstErrorField) firstErrorField = field;
} else {
group.classList.remove('error');
}
});
if (hasError && firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstErrorField.focus(), 600);
return;
}
try {
const res = await fetch('/api/delivery/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sender,
too,
item: itemId,
delivery_type,
track: track
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
// If response isn't JSON, try to get text
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
// Fallback to generic error
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1_2';
statusSpan.className = 'b_5dr47_2';
statusSpan.textContent = 'Ожидает получения';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
</script>
</div>
</div>
</div>
</section>
{{ footer.section(user) }}
{% endblock %}
{% block head %}
{{ header.style() }}
{{ competition.style() }}
{{ profile_nav.style() }}
<style>
/* Base (common) styles */
.c_aihm6{width: 100%;}
.mw{margin: 0 20px;
width: 100%;}
.main__top{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:16px;}
.main__title{ flex: 1; min-width: 260px;}
.main__title > p{ margin: 0 0 10px 0; font-size: 20px; font-weight: 900; color: #0f172a;}
.main__address-label{ font-size: 13px; color: #64748b; margin-bottom: 6px;}
.main__address-value{ font-size: 14px; font-weight: 800; color: #0f172a;}
.b_ya0t5 {
align-self: stretch;
border-radius: 5px;
height: 222px;
}
picture > img {
width: 100%;
object-fit: scale-down;
}
.b_kbsvf {
padding: 10px 20px;
align-items: flex-start;
align-self: stretch;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
}
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_h57rq {
align-items: flex-start;
background: #FDFAFF;
border-radius: 5px;
border: 1px solid #5F1798;
display: flex;
flex-direction: column;
width: 335px;
}
.b_wy3rt { align-items: center; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 15px 0; width: 100%; }
.b_bvl0d { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
/* remove default button outline but keep focus visible */
.password-toggle:focus { outline: 2px solid rgba(95,23,152,0.25); outline-offset: 2px; }
/* ensure svg size fits */
.password-toggle .eye { width: 24px; height: 24px; display: block; }
.file-name-display {width: 100%;padding: 16px 50px 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;color: #555;background: #fff;min-height: 56px;display: flex;align-items: center;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;transition: all 0.3s ease;cursor: pointer;}
/* Hover эффект */
.c_aihm6:hover select, .c_nb6gy:hover textarea, .poster-group:hover .file-name-display {border-color: #5F1798;}
/* Когда файл выбран — делаем текст чуть темнее */
.poster-group.has-file .file-name-display {color: #222;font-weight: 500;}
/* Скрытый input file */
.hidden-file {position: absolute;inset: 0;opacity: 0;cursor: pointer;z-index: 2;}
/* Плавающий лейбл */
.poster-group .floating-label {position: absolute;left: 16px;top: -8px;font-size: 13px;color: #5F1798;}background: #fff;padding: 0 6px;z-index: 3;transition: all 0.3s ease;pointer-events: none;}
/* Когда лейбл должен подняться */
.poster-group select:focus + .floating-label,
.poster-group.has-file .floating-label {top: -8px;font-size: 13px;color: #5F1798;}
/* Иконка справа */
.file-icon {position: absolute;right: 16px;top: 50%;transform: translateY(-50%);z-index: 3;pointer-events: none;}
/* Красная ошибка */
.poster-group.error .file-name-display {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.15);}
.poster-group.error .floating-label {color: #e74c3c !important;}
/* ====================== FLOATING LABELS ====================== */
.floating-group {position: relative;margin-bottom: 1.5rem;}
.floating-label {position: absolute;left: 16px;top: -8px;font-size: 13px;color: #5F1798;transition: all 0.3s ease;pointer-events: none;background: #fff;padding: 0 6px;z-index: 2;}
/* Плавающий лейбл */
.floating-group select:focus + .floating-label, .floating-group textarea:focus + .floating-label,
.floating-group select:not(:placeholder-shown) + .floating-label, .floating-group textarea:not(:placeholder-shown) + .floating-label {top: -8px;font-size: 15px;color: #5F1798;}
/* Красная подсветка — ТОЛЬКО после попытки отправки */
.floating-group.error select, .floating-group.error textarea,
.floating-group.error select[type="file"] {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.38);}
.floating-group.error .floating-label {color: #e74c3c !important;}
/* Основные стили полей */
.c_xhd5k, .c_4ykpn {box-sizing: border-box; width: 100%;padding: 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s ease;background: #fff;}
.c_xhd5k:focus, .c_4ykpn:focus {border-color: #5F1798;outline: none;box-shadow: 0 0 0 3px rgba(95, 23, 152, 0.15);}
.b_wy3rt {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
.b_wy3rt:active {
transform: scale(0.9);
box-shadow: 5px 5px 5px rgba(95, 23, 152, 0.32);
}
article>a{width: 100%;}
picture>img{ width: 100%; object-fit: scale-down; }
.fl{display: flex;justify-content: space-between;width: 100%;}
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.b_wy3rt { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
.b_ornr9 { align-self: stretch; height: 400px; overflow: hidden;}
.b_0jst1 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_vabj9 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_0jst1_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_vabj9_2 { align-items: center; background: #5e17982f; border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_8f3uh { -webkit-box-orient: vertical; -webkit-line-clamp: 1; color: #000; display: -webkit-box; flex: 1 0 0; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.b_9xtz9 { align-items: center; align-self: stretch; display: flex; justify-content: center; }
.b_tfnz2 { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; cursor: pointer; }
.b_a68fq { aspect-ratio: 1/1; border-radius: 50px; height: 50px; width: 50px; object-fit: cover; }
.b_ghosy { align-self: stretch; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
.b_ntazj { align-items: flex-start; display: flex; flex-direction: column; flex: 1 0 0; justify-content: center; }
.b_otttb { align-items: center; align-self: stretch; display: flex; gap: 13px; height: 50px; }
.b_s6ool { white-space: pre-line; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; color: rgba(58, 58, 58, 0.75); display: -webkit-box; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.b_mp1wb { align-self: stretch; color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; }
.b_73p10 { align-items: flex-start; align-self: stretch; background: #FFF; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.c_z3dpx{ padding: 10px 20px;}
.b_ax8vz { align-items: flex-start; border-radius: 10px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; overflow: hidden;}
@media (min-width: 901px) {
.c_9fgdh { align-items: flex-start; border-radius: 10px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; flex-shrink: 0; width: 335px; }
}
@media (max-width: 699px) {
.b_ornr9{height: auto;}
}
</style>
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;
gap: 7px;
margin: 10px 20px;}
.icona{display: flex;
gap: 10px;
align-items: center;}
.icona>span{color: #5F1798;}
.c_hysjg {
color: #0ED646;
font-family: Inter;
/* font-size: 12px; */
font-style: normal;
font-weight: 700;
line-height: normal;
}
/* .c_m876y{align-items: center;
background: rgba(14, 214, 70, 0.20);
border-radius: 30px;
display: flex;
justify-content: center;
padding: 1.5px 10px;} */
/* Mobile */
@media (max-width: 699px) {
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
}
/* Tablet */
@media (min-width: 700px) and (max-width: 900px) {
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.c_nnsh6>a>img{display: none;}
}
/* Desktop */
@media (min-width: 901px) {
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
.c_nnsh6>a>img{display: none;}
}</style>
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_6spaf {
color: #5F1798;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.b_qla2q {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: #000;
display: -webkit-box;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.b_uu2ub { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.b_1fvkt { align-self: stretch; border-radius: 5px; flex-shrink: 0; }
.b_pvw5a { gap: 7px;align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; flex: 1 0 0; justify-content: space-between; padding: 10px 20px 37px 20px; }
.b_xz6em { align-items: center; align-self: stretch; display: flex; gap: 13px; }
.b_ob59y { border-radius: 1em; height: 70px; min-width: 70px; max-width: 150px; vertical-align: top; object-fit: cover;}
.b_ffruz { align-items: flex-start; display: flex; flex-direction: column; flex: 1 0 0; justify-content: center; }
.b_09est { color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 700; line-height: normal; }
.b_wy3rt {
align-items: center;
background: #5F1798;
border-radius: 5px;
display: flex;
justify-content: center;
padding: 15px 0;
width: 100%;
}
.b_bvl0d {
color: #FFF;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
/* Mobile */
@media (max-width: 699px) {
.c_3g8gh { align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; flex-shrink: 0; justify-content: space-between; padding: 10px 20px 37px 20px; }
.c_4b1hy { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
}
/* Tablet */
@media (min-width: 700px) and (max-width: 900px) {
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.c_nnsh6>a>img{display: none;}
}
/* Desktop */
@media (min-width: 901px) {
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
.c_nnsh6>a>img{display: none;}
}</style>
{{ footer.style() }}
{% endblock %}

View File

@ -7,6 +7,90 @@
{% block title %}Приглашения{% endblock %}
{% macro render_doc(doc) %}
<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}}">
<figcaption class="c_3g8gh b_pvw5a" data-id="{{doc._id}}">
{% if doc.status == 'Ожидает отправки' %}
<div class="b_thlt1">
<span class="b_5dr47">{{doc.status}}</span>
</div>
{% elif doc.status == 'Получено' %}
<div class="b_thlt1">
<span class="b_5dr47">{{doc.status}}</span>
</div>
{% elif doc.status == 'Ожидает получения' %}
<div class="b_thlt1_2">
<span class="b_5dr47_2">{{doc.status}}</span>
</div>
{% else %}
<div class="b_thlt1_2">
<span class="b_5dr47_2">{{doc.status}}</span>
</div>
{% endif %}
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
<h2 class="c_9hvtd b_f5fb2">{{doc.title}}</h2>
{% if doc.type == 'exhibition' and doc.get('exhibition') %}
<p class="b_6spaf">{{doc.get('exhibition').title}}</p>
<div class="b_xz6em">
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Организатор</span>
<span class="c_zm0m4 b_09est">{{doc.receiver.username}}</span>
</div>
</div>
{% else %}
{% if doc.address %}
<p class="b_6spaf">Адрес: {{doc.address}}</p>
{% endif %}
{% if doc.back_address %}
<p class="b_6spaf">Адрес для возврата: {{doc.back_address}}</p>
{% endif %}
{% if doc.delivery_type %}
<p class="b_6spaf">Способ доставки: {{doc.delivery_type}}</p>
{% endif %}
{% if doc.track %}
<p class="b_6spaf">Трэк-номер: {{doc.track}}</p>
{% endif %}
{% if doc.sender %}
<div class="b_xz6em">
<img src="/a/{{doc.sender._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Отправитель</span>
<span class="c_zm0m4 b_09est">{{doc.sender.username}}</span>
</div>
</div>
{% endif %}
{% if doc.receiver %}
<div class="b_xz6em">
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
<div class="c_9hjdk b_ffruz">
<span class="b_qla2q">Получатель</span>
<span class="c_zm0m4 b_09est">{{doc.receiver.username}}</span>
<span class="b_qla2q">+7 *** *** {{ doc.receiver.phone[-4:] }}</span>
</div>
</div>
{% endif %}
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
<span class="b_bvl0d">Отправить</span>
</button>
{% endif %}
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
<span class="b_bvl0d">Получить</span>
</button>
{% endif %}
{% endif %}
<p class="b_6spaf">
{{ (doc._id.generation_time.strftime('%d.%m.%Y')) if doc._id else ''}}
<b>{{ '%02d' % ((doc._id.generation_time.hour + 3) % 24) if doc._id else '' }}:{{ '%02d' % doc._id.generation_time.minute if doc._id else '' }}</b>
</p>
</figcaption>
</figure>
{% endmacro %}
{% macro render_post(doc) %}
<figure class="c_4b1hy b_uu2ub">
<img class="b_1fvkt" src="/b/posts/{{doc.medias[0]}}">
@ -50,11 +134,12 @@
{% block content %}
{{ header.section(user) }}
<section class="c_dg3k4">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'invitation') }}
<div class="c_tihtk" id="main">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'invitation') }}
<div class="c_tihtk" id="main">
{% if doc.get('exhibition')%}
<article class="c_9fgdh b_ax8vz" id="l{{doc._id}}">
<div class="c_z3dpx b_73p10">
<div class="b_vabj9_2">
@ -75,24 +160,225 @@
</div>
{{render_links(doc)}}
<p>Картин выбрано: <span id="lenSelected">0</span></p>
<button
onclick="accept_inv('{{doc._id}}')"
class="btn"
style="width: 100%; box-sizing: border-box;"
>
<button onclick="accept_inv('{{doc._id}}')" class="btn" style="width: 100%; box-sizing: border-box;">
Принять условия
</button>
{% set user_contact = doc.get('user') %}
{{render_user_contacts(user_contact)}}
{{ render_user_contacts(user_contact) }}
<p class="c_drpqj b_mp1wb">{{ (doc._id.generation_time.strftime('%d.%m.%Y')) if doc._id else ''}}</p>
</div>
</article>
{% for post in doc.get('posts')%}
{{render_post(post)}}
{% endfor %}
{%endif%}
{% if doc.get('type_')=='paint' %}
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
<article class="c_9fgdh b_ax8vz">
<div class="c_z3dpx b_73p10">
<header class="c_m876y b_9xtz9">
<h3 class="b_8f3uh">Настройки отправки</h3>
</header>
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
<div class="b_73p10">
<div class="c_aihm6 floating-group">
<select class="c_xhd5k" type="text" id="delivery_type" name="delivery_type" required>
<option>СДЭК</option>
<option>Самостоятельно</option>
</select>
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
</div>
<div class="c_aihm6 floating-group">
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
data-toast="Поле Трэк-номер должно быть заполнено">
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
</div>
</div>
</div>
</article>
{% endif %}
{% for doc in data if doc.status in statuses %}
{{ render_doc(doc) }}
{% endfor %}
{% for doc in data if doc.status not in statuses %}
{{ render_doc(doc) }}
{% endfor %}
<script defer>
const query = window.location.href.split('?')[1] || '';
let exhibition_id = null;
if (query) {
const parts = query.split('=');
if (parts[0] === 'exhibition_id' && parts[1] !== 'None') {
exhibition_id = parts[1];
}
}
function filterFigure(exhibition_id) {
document.getElementById('main').querySelectorAll('figure').forEach(figure => {
if (!exhibition_id || figure.dataset.exhibition_id === exhibition_id) {
figure.style.display = 'flex';
console.log(figure.dataset.exhibition_id, exhibition_id, 'flex')
} else {
console.log(figure.dataset.exhibition_id, exhibition_id)
figure.style.display = 'none';
}
});
}
if (exhibition_id) {
document.getElementById('exhibition_id').value = exhibition_id;
}
filterFigure(exhibition_id);
</script>
<script>
const deliveryType = document.getElementById('delivery_type');
const track = document.getElementById('track');
function toggleTrack() {
const isSelf = deliveryType.value === 'Самостоятельно';
if (isSelf) {
track.required = false;
track.value = '';
} else {
track.required = true;
}
}
deliveryType.addEventListener('change', toggleTrack);
toggleTrack();
</script>
<script>
async function btnAccept(btn, itemId){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
try {
const res = await fetch('/api/delivery/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: itemId,
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1';
statusSpan.className = 'b_5dr47';
statusSpan.textContent = 'Получено';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
async function btnSend(btn, itemId, sender, too){
const figcaption = btn.parentElement;
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span')
const delivery_type = document.getElementById('delivery_type').value;
const trackValue = document.getElementById('track');
let track = ''
if (delivery_type === 'СДЭК' && trackValue) {
track = trackValue.value
}
const requiredInputs = document.querySelectorAll('input[required]');
const requiredTextareas = document.querySelectorAll('textarea[required]');
const requiredFields = [...requiredInputs, ...requiredTextareas];
let firstErrorField = null;
let hasError = false;
requiredFields.forEach(field => {
const group = field.closest('.floating-group');
if (!group) return; // skip if no group wrapper
let isEmpty = false;
if (field.type === 'file') {
isEmpty = !field.files || field.files.length === 0;
} else {
isEmpty = !field.value || !field.value.trim();
}
if (isEmpty) {
group.classList.add('error');
hasError = true;
showToast({message: field.dataset.toast, duration:2500});
if (!firstErrorField) firstErrorField = field;
} else {
group.classList.remove('error');
}
});
if (hasError && firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstErrorField.focus(), 600);
return;
}
try {
const res = await fetch('/api/delivery/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sender,
too,
item: itemId,
delivery_type,
track: track
})
});
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
try {
const errorData = await res.json();
errorMessage = errorData.detail || errorMessage;
} catch (e) {
// If response isn't JSON, try to get text
try {
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {
// Fallback to generic error
}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1_2';
statusSpan.className = 'b_5dr47_2';
statusSpan.textContent = 'Ожидает получения';
btn.style.display = 'none'
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
};
</script>
{% endif %}
</div>
</div>
</div>
</section>
<script>
const LS_KEY = 'selected_posts_ids_{{doc._id}}';
@ -244,7 +530,7 @@
setTimeout(() => {
localStorage.removeItem(LS_KEY);
window.location.href = '/profile/delivery#main?exhibition_id={{doc.get("exhibition_id")}}';
window.location.href = '/profile/delivery/exhibition/{{doc.get("exhibition_id")}}#main';
}, 5000);
} catch (e) {
@ -307,14 +593,263 @@
</div>
{% endmacro %}
{% macro render_links(doc) %}
{% if doc.get('exhibition')%}
{% if doc.get('exhibition').get('tizer_file_id') or doc.get('exhibition').get('tizer_link') %}
<a class="btn" href="{% if doc.get('exhibition') and doc.get('exhibition').get('tizer_link') %}{{doc.get('exhibition').tizer_link}}{% else %}/b/exhibition/{{ doc.get('exhibition').tizer_file_id }}{% endif %}" style="width: 100%;">Релиз выставки</a>
{%endif%}
<a class="btn" href="{% if doc.get('exhibition') and doc.get('exhibition').get('rules_link') %}{{doc.get('exhibition').rules_link}}{% else %}/b/exhibition/{{ doc.get('exhibition').rules_file_id }}{% endif %}" style="width: 100%;">Условия выставки</a>
{%endif%}
{% endmacro %}
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style>
/* Base (common) styles */
.c_aihm6{width: 100%;}
.mw{margin: 0 20px;
width: 100%;}
.main__top{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:16px;}
.main__title{ flex: 1; min-width: 260px;}
.main__title > p{ margin: 0 0 10px 0; font-size: 20px; font-weight: 900; color: #0f172a;}
.main__address-label{ font-size: 13px; color: #64748b; margin-bottom: 6px;}
.main__address-value{ font-size: 14px; font-weight: 800; color: #0f172a;}
.b_ya0t5 {
align-self: stretch;
border-radius: 5px;
height: 222px;
}
picture > img {
width: 100%;
object-fit: scale-down;
}
.b_kbsvf {
padding: 10px 20px;
align-items: flex-start;
align-self: stretch;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
}
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_h57rq {
align-items: flex-start;
background: #FDFAFF;
border-radius: 5px;
border: 1px solid #5F1798;
display: flex;
flex-direction: column;
width: 335px;
}
.b_wy3rt { align-items: center; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 15px 0; width: 100%; }
.b_bvl0d { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
/* remove default button outline but keep focus visible */
.password-toggle:focus { outline: 2px solid rgba(95,23,152,0.25); outline-offset: 2px; }
/* ensure svg size fits */
.password-toggle .eye { width: 24px; height: 24px; display: block; }
.file-name-display {width: 100%;padding: 16px 50px 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;color: #555;background: #fff;min-height: 56px;display: flex;align-items: center;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;transition: all 0.3s ease;cursor: pointer;}
/* Hover эффект */
.c_aihm6:hover select, .c_nb6gy:hover textarea, .poster-group:hover .file-name-display {border-color: #5F1798;}
/* Когда файл выбран — делаем текст чуть темнее */
.poster-group.has-file .file-name-display {color: #222;font-weight: 500;}
/* Скрытый input file */
.hidden-file {position: absolute;inset: 0;opacity: 0;cursor: pointer;z-index: 2;}
/* Плавающий лейбл */
.poster-group .floating-label {position: absolute;left: 16px;top: -8px;font-size: 13px;color: #5F1798;}background: #fff;padding: 0 6px;z-index: 3;transition: all 0.3s ease;pointer-events: none;}
/* Когда лейбл должен подняться */
.poster-group select:focus + .floating-label,
.poster-group.has-file .floating-label {top: -8px;font-size: 13px;color: #5F1798;}
/* Иконка справа */
.file-icon {position: absolute;right: 16px;top: 50%;transform: translateY(-50%);z-index: 3;pointer-events: none;}
/* Красная ошибка */
.poster-group.error .file-name-display {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.15);}
.poster-group.error .floating-label {color: #e74c3c !important;}
/* ====================== FLOATING LABELS ====================== */
.floating-group {position: relative;margin-bottom: 1.5rem;}
.floating-label {position: absolute;left: 16px;top: -8px;font-size: 13px;color: #5F1798;transition: all 0.3s ease;pointer-events: none;background: #fff;padding: 0 6px;z-index: 2;}
/* Плавающий лейбл */
.floating-group select:focus + .floating-label, .floating-group textarea:focus + .floating-label,
.floating-group select:not(:placeholder-shown) + .floating-label, .floating-group textarea:not(:placeholder-shown) + .floating-label {top: -8px;font-size: 15px;color: #5F1798;}
/* Красная подсветка — ТОЛЬКО после попытки отправки */
.floating-group.error select, .floating-group.error textarea,
.floating-group.error select[type="file"] {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.38);}
.floating-group.error .floating-label {color: #e74c3c !important;}
/* Основные стили полей */
.c_xhd5k, .c_4ykpn {box-sizing: border-box; width: 100%;padding: 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s ease;background: #fff;}
.c_xhd5k:focus, .c_4ykpn:focus {border-color: #5F1798;outline: none;box-shadow: 0 0 0 3px rgba(95, 23, 152, 0.15);}
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
.c_aq9et:active {
transform: scale(0.9);
box-shadow: 5px 5px 5px rgba(95, 23, 152, 0.32);
}
article>a{width: 100%;}
picture>img{ width: 100%; object-fit: scale-down; }
.fl{display: flex;justify-content: space-between;width: 100%;}
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.c_aq9et { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
.b_ornr9 { align-self: stretch; height: 400px; overflow: hidden;}
.b_0jst1 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_vabj9 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_0jst1_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_vabj9_2 { align-items: center; background: #5e17982f; border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_8f3uh { -webkit-box-orient: vertical; -webkit-line-clamp: 1; color: #000; display: -webkit-box; flex: 1 0 0; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.b_9xtz9 { align-items: center; align-self: stretch; display: flex; justify-content: center; }
.b_tfnz2 { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; cursor: pointer; }
.b_a68fq { aspect-ratio: 1/1; border-radius: 50px; height: 50px; width: 50px; object-fit: cover; }
.b_ghosy { align-self: stretch; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
.b_ntazj { align-items: flex-start; display: flex; flex-direction: column; flex: 1 0 0; justify-content: center; }
.b_otttb { align-items: center; align-self: stretch; display: flex; gap: 13px; height: 50px; }
.b_s6ool { white-space: pre-line; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; color: rgba(58, 58, 58, 0.75); display: -webkit-box; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.b_mp1wb { align-self: stretch; color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; }
.b_73p10 { align-items: flex-start; align-self: stretch; background: #FFF; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.c_z3dpx{ padding: 10px 20px;}
.b_ax8vz { align-items: flex-start; border-radius: 10px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; overflow: hidden;}
@media (min-width: 901px) {
.c_9fgdh { align-items: flex-start; border-radius: 10px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; flex-shrink: 0; width: 335px; }
}
@media (max-width: 699px) {
.b_ornr9{height: auto;}
}
</style>
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;
gap: 7px;
margin: 10px 20px;}
.icona{display: flex;
gap: 10px;
align-items: center;}
.icona>span{color: #5F1798;}
.c_hysjg {
color: #0ED646;
font-family: Inter;
/* font-size: 12px; */
font-style: normal;
font-weight: 700;
line-height: normal;
}
/* .c_m876y{align-items: center;
background: rgba(14, 214, 70, 0.20);
border-radius: 30px;
display: flex;
justify-content: center;
padding: 1.5px 10px;} */
/* Mobile */
@media (max-width: 699px) {
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
}
/* Tablet */
@media (min-width: 700px) and (max-width: 900px) {
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.c_nnsh6>a>img{display: none;}
}
/* Desktop */
@media (min-width: 901px) {
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
.c_nnsh6>a>img{display: none;}
}</style>
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47_2 { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_6spaf {
color: #5F1798;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.b_qla2q {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: #000;
display: -webkit-box;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.b_uu2ub { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.b_1fvkt { align-self: stretch; border-radius: 5px; flex-shrink: 0; }
.b_pvw5a { gap: 7px;align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; flex: 1 0 0; justify-content: space-between; padding: 10px 20px 37px 20px; }
.b_xz6em { align-items: center; align-self: stretch; display: flex; gap: 13px; }
.b_ob59y { border-radius: 1em; height: 70px; min-width: 70px; max-width: 150px; vertical-align: top; object-fit: cover;}
.b_ffruz { align-items: flex-start; display: flex; flex-direction: column; flex: 1 0 0; justify-content: center; }
.b_09est { color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 700; line-height: normal; }
.b_wy3rt {
align-items: center;
background: #5F1798;
border-radius: 5px;
display: flex;
justify-content: center;
padding: 15px 0;
width: 100%;
}
.b_bvl0d {
color: #FFF;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
/* Mobile */
@media (max-width: 699px) {
.c_3g8gh { align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; flex-shrink: 0; justify-content: space-between; padding: 10px 20px 37px 20px; }
.c_4b1hy { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
}
/* Tablet */
@media (min-width: 700px) and (max-width: 900px) {
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.c_nnsh6>a>img{display: none;}
}
/* Desktop */
@media (min-width: 901px) {
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
.c_nnsh6>a>img{display: none;}
}</style>
<style>/* Base (common) styles */
.c_aq9et {
cursor: pointer;

View File

@ -25,6 +25,9 @@
<header class="c_m876y b_9xtz9">
<h3 class="c_hysjg b_8f3uh">{{ doc.title }}</h3>
</header>
{% if doc.get('type_')=='paint' %}
<a class="btn ba" href="/profile/invitation/{{doc._id}}#main" style="width: 100%;">Открыть</a>
{% endif %}
{% if doc.get('type') == 'Выставка' %}
<p class="c_2fr39 b_tfnz2">{{doc.get('exhibition').title }}</p>
<p>Место проведения: {{ doc.get('exhibition').get('place') }}</p>
@ -36,24 +39,29 @@
doc.get('exhibition').date_finish else
''}}</span>
</div>
{% if doc.get('exhibition').get('tizer_file_id') or doc.get('exhibition').get('tizer_link') %}
<a class="btn"
<a class="btn ba"
href="{% if doc.get('exhibition') and doc.get('exhibition').get('tizer_link') %}{{doc.get('exhibition').tizer_link}}{% else %}/b/exhibition/{{ doc.get('exhibition').tizer_file_id }}{% endif %}"
style="width: 100%;">Релиз выставки</a>
{% endif %}
<p class="c_2fr39 b_tfnz2">Принять условия нужно до {{doc.get('exhibition').datetime_start.strftime('%d.%m.%Y')}}</p>
{% if doc.get('accept') == False and now < doc.get('exhibition').datetime_start %}
<a class="btn" href="/profile/invitation/{{doc._id}}#main" style="width: 100%;">Открыть</a>
<a class="btn ba" href="/profile/invitation/{{doc._id}}#main" style="width: 100%;">Открыть</a>
<!-- <button onclick="accept_inv('{{doc._id}}')" class="btn" href="/profile/delivery#main" style="width: 100%; box-sizing: border-box;">Принять условия</button> -->
{% else %}
<a class="btn"
<a class="btn ba"
href="{% if doc.get('exhibition') and doc.get('exhibition').get('rules_link') %}{{doc.get('exhibition').rules_link}}{% else %}/b/exhibition/{{ doc.get('exhibition').rules_file_id }}{% endif %}"
style="width: 100%;">Условия выставки</a>
{% endif %}
{% else %}
{% if doc.get('type_')=='paint' %}
<p class="c_2fr39 b_tfnz2">"{{ doc.get('item') }}"</p>
{%else%}
<p class="c_2fr39 b_tfnz2">"{{ doc.get('item') }}" просьба связаться с организатором для обсуждения
дальнейших действий</p>
{% endif %}
{% endif %}
<!-- <p class="c_sihck b_s6ool">От</p> -->
{% set user_contact = doc.get('user') %}
{{render_user_contacts(user_contact)}}
@ -139,7 +147,8 @@ async function accept_inv(inv_id) {
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style>/* Base (common) styles */
<style>
/* Base (common) styles */
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;

View File

@ -17,7 +17,7 @@
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'news') }}
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1" href="/form/news"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1 ba" href="/form/news"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_tihtk">
{% for new in news %}
<figure class="c_57hjd b_h57rq">

View File

@ -23,11 +23,11 @@
<h2 class="c_b2jz8 b_5ckkw">{{competition.title}}</h2>
</header>
</div>
<a class="c_d4rhd b_wy3rt" href="/competition/{{competition._id}}">
<a class="c_d4rhd b_wy3rt ba" href="/competition/{{competition._id}}">
<span class="c_4sl8i b_bvl0d">Открыть страницу конкурса</span>
</a>
{% if competition.status == 0 %}
<div class="c_d4rhd b_wy3rt" id="stop-btn">
<div class="c_d4rhd b_wy3rt ba" id="stop-btn">
<span class="c_4sl8i b_bvl0d">Остановить прием заявок на конкурс</span>
</div>
@ -58,7 +58,7 @@
});
</script>
{% elif competition.status == 1 %}
<div class="c_d4rhd b_wy3rt" id="stop-btn"><span class="c_4sl8i b_bvl0d">Завершить конкурс</span>
<div class="c_d4rhd b_wy3rt ba" id="stop-btn"><span class="c_4sl8i b_bvl0d">Завершить конкурс</span>
</div>
<script>
const competitionId = '{{competition._id}}';
@ -87,7 +87,7 @@
});
</script>
{% elif competition.status == 2 %}
<div class="c_d4rhd b_wy3rt"><span class="c_4sl8i b_bvl0d">Конкурс завершен</span></div>
<div class="c_d4rhd b_wy3rt ba"><span class="c_4sl8i b_bvl0d">Конкурс завершен</span></div>
{%endif%}
</div>
<div class="c_ntoq3 b_6ycbz">
@ -278,7 +278,8 @@ document.querySelectorAll('.b_k2cle button').forEach(button => {
{{ header.style() }}
{{card.style()}}
{{ profile_nav.style() }}
<style>/* Base (common) styles */
<style>
/* Base (common) styles */
/* Активное состояние */
.c_fx6lz.active,
.c_deket.active {

View File

@ -26,10 +26,10 @@
</svg> Вернуться</h2>
</header>
</a>
<a class="c_d4rhd b_wy3rt" href="/competition/{{competition._id}}">
<a class="c_d4rhd b_wy3rt ba" href="/competition/{{competition._id}}">
<span class="c_4sl8i b_bvl0d">Открыть страницу конкурса</span>
</a>
<div class="c_d4rhd b_wy3rt" id="stop-btn">
<div class="c_d4rhd b_wy3rt ba" id="stop-btn">
<span class="c_4sl8i b_bvl0d">Остановить прием заявок на конкурс</span>
</div>
<a href="/profile/partner/competition/{{competition._id}}#main" id="{{member.id}}">

View File

@ -16,7 +16,7 @@
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'competitions') }}
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1" href="/form/competition"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1 ba" href="/form/competition"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_tihtk">
{% for competition in cards %}
{{ card.section(competition, true, link='/profile/partner/competition/{}#main'.format(competition._id)) }}

View File

@ -102,6 +102,11 @@
<span class="b_al8e7">Получить</span>
</button>
{% endif %}
{% if post.status in ['Возврат художнику']%}
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
<span class="b_al8e7">Отправить</span>
</a>
{% endif %}
</div>
</article>
{% endfor %}

View File

@ -15,7 +15,7 @@
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'exhibitions') }}
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1" href="/form/exhibition"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1 ba" href="/form/exhibition"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_tihtk">
{% for i in data %}
<article class="c_9fgdh b_ax8vz">

View File

@ -16,7 +16,7 @@
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'paint') }}
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1" href="/form/post"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_3ihvi" id="main"><a class="c_ctfod b_289i1 ba" href="/form/post"><span class="c_d2l0b b_al8e7">Добавить</span></a></div>
<div class="c_tihtk">
{% for i in cards %}
{{card.section(i, loop.index, user)}}

View File

@ -115,9 +115,9 @@ document.addEventListener('DOMContentLoaded', () => {
.b_v63fu { align-items: center; background: #F39318; border-radius: 5px; display: flex; justify-content: center; padding: 15px 55px; }
.b_h9dsc { display: none; align-items: center; align-self: stretch; background: #FFF; border-radius: 5px; flex-direction: column; gap: 15px; justify-content: center; padding: 10px 0; }
.b_ld219 { color: #171717; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 700; line-height: normal; }
.b_ny6vg { align-items: center; background: #FFF; box-shadow: 0 -1px 0 0 #5F1798 inset; display: flex; height: 35px; justify-content: center; padding: 0 10px; }
.b_ny6vg { align-items: center; box-shadow: 0 -1px 0 0 #5F1798 inset; display: flex; height: 35px; justify-content: center; padding: 0 10px; }
.b_v15gw { color: #969696; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 700; line-height: normal; }
.b_cros1 { align-items: center; background: #FFF; display: flex; height: 35px; justify-content: center; padding: 0 10px; }
.b_cros1 { align-items: center; display: flex; height: 35px; justify-content: center; padding: 0 10px; }
.b_289i1 { align-items: center; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 13px 15px; width: calc(100% - 30px); }
.b_3kfnv { align-self: stretch; height: 400px; }
.b_xcy5t { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }

View File

@ -48,8 +48,7 @@
" load_env('env')\n",
"except:\n",
" load_env('env_example')\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -82,8 +81,7 @@
"import asyncio\n",
"import json\n",
"from datetime import datetime, time, timedelta, timezone\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -95,8 +93,7 @@
"Image.MAX_IMAGE_PIXELS = 50_000_000\n",
"TMP_DIR = Path(\"/tmp\")\n",
"TMP_DIR.mkdir(parents=True, exist_ok=True)"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -106,8 +103,7 @@
"outputs": [],
"source": [
"celery_app_tg = Celery('tg', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/tg', task_track_started=True )\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -124,8 +120,7 @@
" if chunk:\n",
" chunks.append(chunk)\n",
" return b\"\".join(chunks)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -146,8 +141,7 @@
" img = await fetch_bytes(client, f\"https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}\")\n",
" new_tg = (channel_name, BytesIO(img))\n",
" print(channel_name)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -217,8 +211,7 @@
" images.append(img)\n",
"\n",
" return images"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -243,8 +236,7 @@
" \"media\": f\"attach://file{i}\",\n",
" })\n",
" return media"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -322,8 +314,7 @@
" await asyncio.sleep(0.8 * attempt)\n",
"\n",
" raise last_exc\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -344,8 +335,7 @@
" # The API returns JSON like {\"ok\":true,\"result\":true}\n",
" data = response.json()\n",
" return data.get(\"ok\") and data.get(\"result\") is True"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -374,8 +364,7 @@
" # httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
" except Exception as exc:\n",
" raise self.retry(exc=exc, countdown=30)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -404,8 +393,7 @@
" httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
" except Exception as exc:\n",
" raise self.retry(exc=exc, countdown=30)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -426,8 +414,7 @@
" asyncio.run(new_channel_reg(chat_id))\n",
" except Exception as exc:\n",
" raise self.retry(exc=exc, countdown=30)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -466,8 +453,7 @@
"\n",
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
" f.write('\\n\\n'.join(app))\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -477,8 +463,7 @@
"outputs": [],
"source": [
"!celery -A tg.celery_app_tg worker --loglevel=info"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -528,8 +513,7 @@
" else:\n",
" print(\"Failed to delete message.\")\n",
"\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -537,8 +521,7 @@
"id": "416f454c",
"metadata": {},
"outputs": [],
"source": [],
"display_name": ".venv"
"source": []
}
],
"metadata": {
@ -557,7 +540,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

1
tg.py
View File

@ -1,3 +1,4 @@
# `tg.py` generate from `tg.ipynb` by `build.ipynb`
# TG
## Class

161
vk.ipynb
View File

@ -53,8 +53,7 @@
" load_env('env_example')\n",
" except Exception:\n",
" pass"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -86,8 +85,7 @@
"from fastapi import APIRouter, Depends, Request, Response\n",
"from fastapi.responses import RedirectResponse\n",
"from images import insert_logo\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -105,8 +103,7 @@
"outputs": [],
"source": [
"celery_app_vk = Celery('vk', broker=f'{redis_uri}1', backend=f'{redis_uri}1', task_track_started=True )\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -116,8 +113,7 @@
"outputs": [],
"source": [
"token_manager = TokenManager()\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -158,8 +154,7 @@
"\n",
" # Привязка соцсети к уже залогиненному пользователю\n",
" return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -189,8 +184,7 @@
" images[_id] = (\"photo\", (name, background, \"image/jpeg\"))\n",
" \n",
" return images\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -263,8 +257,7 @@
" await mongo_client[db_name][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec)\n",
" return {\"ok\": True}\n",
"\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -340,8 +333,7 @@
" await mongo_client[db_name][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec)\n",
" return {\"ok\": True}\n",
"\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -394,8 +386,7 @@
" )\n",
"\n",
" return {\"ok\": True}\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -430,8 +421,7 @@
" \n",
" except Exception as exc:\n",
" raise self.retry(exc=exc)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -466,8 +456,7 @@
" \n",
" except Exception as exc:\n",
" raise self.retry(exc=exc)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -502,8 +491,7 @@
" \n",
" except Exception as exc:\n",
" raise self.retry(exc=exc)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -534,8 +522,7 @@
"# timeout=100\n",
"# ).json()\n",
"# await asyncio.sleep(1)\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -566,8 +553,7 @@
"\n",
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
" f.write('\\n\\n'.join(app))\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -581,8 +567,7 @@
"outputs": [],
"source": [
"!celery -A vk.celery_app_vk worker --loglevel=info"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -596,8 +581,7 @@
"outputs": [],
"source": [
"!docker build -t registry.spiritofweb.ru/avtopost-api:latest ."
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -611,8 +595,7 @@
"outputs": [],
"source": [
"!sudo docker image push registry.spiritofweb.ru/avtopost-api:latest"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -626,8 +609,7 @@
"outputs": [],
"source": [
"!kubectl rollout restart deployments/vk-celery -n kultmost"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -641,8 +623,7 @@
"outputs": [],
"source": [
"!echo $REGISTRY_HOST"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -660,8 +641,7 @@
"outputs": [],
"source": [
"stop"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -671,8 +651,7 @@
"outputs": [],
"source": [
"user_id = '681684390000000000000000'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -683,8 +662,7 @@
"source": [
"await redis_client.delete(f\"access_token:{user_id}\")\n",
"await redis_client.delete(f\"tokens:{user_id}\")"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -708,8 +686,7 @@
" },\n",
" upsert=True,\n",
")\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -719,8 +696,7 @@
"outputs": [],
"source": [
"import token_manager as tm"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -731,8 +707,7 @@
"source": [
"from token_manager import TokenManager\n",
"token_manager = TokenManager()"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -744,8 +719,7 @@
"token_manager = TokenManager()\n",
"access_token = await token_manager.get_access_token('644fa8bb0000000000000000')\n",
"access_token"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -756,8 +730,7 @@
"source": [
"item ={'_id': '6a21559cca3ad8a73b652bcb', 'db': 'competitions', 'col': 'competitions'} \n",
"file_ids = ['681684390000000000000000', '6a21559cca3ad8a73b652bc6']"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -768,8 +741,7 @@
"source": [
"docs =[ doc async for doc in mongo_client['social']['social'].find({'user_id':'681684390000000000000000'})]\n",
"doc = docs[1]"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -779,8 +751,7 @@
"outputs": [],
"source": [
"competition = await competitions_col.find_one({'_id':ObjectId('6a21559cca3ad8a73b652bcb')})"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -790,8 +761,7 @@
"outputs": [],
"source": [
"images = await get_images(db_name, file_ids, sign_avatar)"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -833,8 +803,7 @@
" })\n",
" res = resp.json()\n",
" res.update({'doc': doc})"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -844,8 +813,7 @@
"outputs": [],
"source": [
"res"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -912,8 +880,7 @@
" update_spec = {\"$push\": {\"vk\": res}}\n",
" await mongo_client[db_name][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec)\n",
" return {\"ok\": True}\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -952,8 +919,7 @@
" })\n",
" print(resp_wall.json())\n",
" \n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -963,8 +929,7 @@
"outputs": [],
"source": [
"await competitions_col.find_one({'_id': competition['_id']})"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -979,8 +944,7 @@
" \"$push\": {\"vk\": resp_wall.json()}\n",
" },\n",
" )"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -995,8 +959,7 @@
" \"v\": \"5.199\",\n",
" })\n",
"resp.json()['response'][0]"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1006,8 +969,7 @@
"outputs": [],
"source": [
"[doc async for doc in mongo_client['social']['social'].find({'user_id':competition['user']['id']})]"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1025,8 +987,7 @@
"})\n",
"\n",
"z.json()['response']"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1036,8 +997,7 @@
"outputs": [],
"source": [
"#!pip freeze > requirements.txt"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1057,8 +1017,7 @@
" \"body\": \"group_id=191599084&user_id=392892935&role=moderator&is_contact=0&contact_position=&contact_phone=&contact_email=&permissions=&access_token=vk1.a.-kG07OUZgX5FrxtsQrU5QJh8HaP_suYXkqcg\",\n",
" \"method\": \"POST\",\n",
"});"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1076,8 +1035,7 @@
"VK_CLIENT_ID = 51852444\n",
"SCOPES = 'offline,wall,photos,groups'\n",
"redirect_uri = 'https://oauth.vk.ru/blank.html'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1108,8 +1066,7 @@
"url = f'{AUTHORIZE_URL}?responsetype_item=code&client_id={VK_CLIENT_ID}&scope={SCOPES}&redirect_uri={redirect_uri}&state={state}&code_challenge={code_challenge}&code_challenge_method=S256'\n",
"\n",
"print(f\"\\n\\n{url}\\n\\n\")"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1121,8 +1078,7 @@
"\n",
"code='vk2.a.---'\n",
"device_id=''"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1133,8 +1089,7 @@
"source": [
"refresh_token = \"vk2.a.-----\"\n",
"device_id = \"-\"\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1153,8 +1108,7 @@
" 'state': os.urandom(32).hex(),\n",
"})\n",
"z.json()"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1165,8 +1119,7 @@
"source": [
"CLIENT_ID = 51852444\n",
"OAUTH_URL = 'https://id.vk.com/oauth2/auth'"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1187,8 +1140,7 @@
" 'redirect_uri': 'https://oauth.vk.com/blank.html'\n",
"})\n",
"r.json()\n"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1209,8 +1161,7 @@
"\n",
"z=T()\n",
"z.json()"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1221,8 +1172,7 @@
"source": [
"refresh_token=r.json()['refresh_token']\n",
"refresh_token"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1241,8 +1191,7 @@
" 'state': os.urandom(32).hex(),\n",
"})\n",
"z.json()"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1252,8 +1201,7 @@
"outputs": [],
"source": [
"print(f'\\n\\n{z.json()['refresh_token']}\\n\\n')"
],
"display_name": ".venv"
]
},
{
"cell_type": "code",
@ -1263,8 +1211,7 @@
"outputs": [],
"source": [
"users.search"
],
"display_name": ".venv"
]
},
{
"cell_type": "markdown",
@ -1650,7 +1597,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
"version": "3.13.5"
}
},
"nbformat": 4,

1
vk.py
View File

@ -1,3 +1,4 @@
# `vk.py` generate from `vk.ipynb` by `build.ipynb`
# VK
#ignore