Merge pull request 'production' (#2) from production into main
Reviewed-on: http://127.0.0.1:30300/kultmost/avtopost.kultmost.ru/pulls/2
This commit is contained in:
commit
b023fd00d2
177
build.ipynb
177
build.ipynb
@ -12,7 +12,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!pip install -r requirements.txt"
|
"!pip install -r requirements.txt"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -72,7 +73,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"chmod +x .git/hooks/pre-commit\n"
|
"chmod +x .git/hooks/pre-commit\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -120,7 +122,8 @@
|
|||||||
" load_env('env')\n",
|
" load_env('env')\n",
|
||||||
"except:\n",
|
"except:\n",
|
||||||
" load_env('env_example')"
|
" load_env('env_example')"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -132,7 +135,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 1,
|
"execution_count": null,
|
||||||
"id": "2809c42c",
|
"id": "2809c42c",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -161,7 +164,8 @@
|
|||||||
" f.write('\\n\\n'.join(app))\n",
|
" f.write('\\n\\n'.join(app))\n",
|
||||||
"\n",
|
"\n",
|
||||||
"build()"
|
"build()"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -195,7 +199,8 @@
|
|||||||
"kubectl -n container-registry create secret generic registry-basic-auth \\\n",
|
"kubectl -n container-registry create secret generic registry-basic-auth \\\n",
|
||||||
" --from-file=auth=/tmp/registry.htpasswd \\\n",
|
" --from-file=auth=/tmp/registry.htpasswd \\\n",
|
||||||
" --dry-run=client -o yaml | kubectl apply -f -\n"
|
" --dry-run=client -o yaml | kubectl apply -f -\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -209,7 +214,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"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)\""
|
"!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",
|
"cell_type": "code",
|
||||||
@ -224,7 +230,8 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"!kubectl create namespace kultmost\n",
|
"!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)\""
|
"!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",
|
"cell_type": "code",
|
||||||
@ -238,7 +245,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker login $REGISTRY_HOST -u 'user' -p $REGISTRY_PASS"
|
"!docker login $REGISTRY_HOST -u 'user' -p $REGISTRY_PASS"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -260,7 +268,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker pull $REGISTRY_HOST/python:3.13-alpine"
|
"!docker pull $REGISTRY_HOST/python:3.13-alpine"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -274,7 +283,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker pull $REGISTRY_HOST/python:3.13-slim"
|
"!docker pull $REGISTRY_HOST/python:3.13-slim"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -296,7 +306,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker build -t $REGISTRY_HOST/api-tg:latest tg/."
|
"!docker build -t $REGISTRY_HOST/api-tg:latest tg/."
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -310,7 +321,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker push $REGISTRY_HOST/api-tg:latest"
|
"!docker push $REGISTRY_HOST/api-tg:latest"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -332,7 +344,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker build -t $REGISTRY_HOST/avtopost-api:latest ."
|
"!docker build -t $REGISTRY_HOST/avtopost-api:latest ."
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -346,7 +359,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker image push $REGISTRY_HOST/avtopost-api:latest"
|
"!docker image push $REGISTRY_HOST/avtopost-api:latest"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -360,7 +374,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl rollout restart deployments/kultmost-api -n kultmost"
|
"!kubectl rollout restart deployments/kultmost-api -n kultmost"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -382,7 +397,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl rollout restart deployments/vk-celery -n kultmost"
|
"!kubectl rollout restart deployments/vk-celery -n kultmost"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -396,7 +412,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl rollout restart deployments/email-celery -n kultmost"
|
"!kubectl rollout restart deployments/email-celery -n kultmost"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -421,7 +438,8 @@
|
|||||||
"docker pull gitea/gitea:1.22\n",
|
"docker pull gitea/gitea:1.22\n",
|
||||||
"docker tag gitea/gitea:1.22 $REGISTRY_HOST/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"
|
"docker push $REGISTRY_HOST/gitea/gitea:1.22"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -445,7 +463,8 @@
|
|||||||
"docker tag busybox:1.36 $REGISTRY_HOST/busybox:1.36\n",
|
"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 rabbitmq:3.11-management $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||||
"docker tag alpine:3.14 $REGISTRY_HOST/alpine:3.14"
|
"docker tag alpine:3.14 $REGISTRY_HOST/alpine:3.14"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -464,7 +483,8 @@
|
|||||||
"docker push $REGISTRY_HOST/busybox:1.36\n",
|
"docker push $REGISTRY_HOST/busybox:1.36\n",
|
||||||
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||||
"docker push $REGISTRY_HOST/alpine:3.14"
|
"docker push $REGISTRY_HOST/alpine:3.14"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -486,7 +506,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker build -t $REGISTRY_HOST/ssh-tunnel:2 tunnel/"
|
"!docker build -t $REGISTRY_HOST/ssh-tunnel:2 tunnel/"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -500,7 +521,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!docker push $REGISTRY_HOST/ssh-tunnel:2"
|
"!docker push $REGISTRY_HOST/ssh-tunnel:2"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -514,7 +536,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl create secret generic ssh-id-rsa --from-file=id_rsa=/home/user/.ssh/id_rsa -n kultmost"
|
"!kubectl create secret generic ssh-id-rsa --from-file=id_rsa=/home/user/.ssh/id_rsa -n kultmost"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -528,7 +551,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl apply -n kultmost -f k8s/stunnel.yaml"
|
"!kubectl apply -n kultmost -f k8s/stunnel.yaml"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -550,7 +574,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!envsubst < k8s/prod.yaml | kubectl apply -n kultmost -f -"
|
"!envsubst < k8s/prod.yaml | kubectl apply -n kultmost -f -"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -564,7 +589,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!envsubst < k8s/kultmost.yaml | kubectl apply -n kultmost -f -"
|
"!envsubst < k8s/kultmost.yaml | kubectl apply -n kultmost -f -"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -578,7 +604,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!envsubst < k8s/mq.yaml | kubectl apply -n kultmost -f -"
|
"!envsubst < k8s/mq.yaml | kubectl apply -n kultmost -f -"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -592,7 +619,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!envsubst < k8s/gitea.yaml | kubectl apply -n gitea -f -"
|
"!envsubst < k8s/gitea.yaml | kubectl apply -n gitea -f -"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -606,7 +634,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!kubectl get pod -n kultmost"
|
"!kubectl get pod -n kultmost"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -631,7 +660,8 @@
|
|||||||
"echo \"$INIT_SSH'bash -s' <<'SCRIPT'\"\n",
|
"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 \"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\""
|
"echo \"SCRIPT\""
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -654,7 +684,8 @@
|
|||||||
"echo \"clear\"\n",
|
"echo \"clear\"\n",
|
||||||
"echo \"history -c\"\n",
|
"echo \"history -c\"\n",
|
||||||
"echo \"history -w\""
|
"echo \"history -w\""
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -684,7 +715,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!rsync -avz -ogp --chown=www-data:www-data ./static/ kultmost:/var/www/static/"
|
"!rsync -avz -ogp --chown=www-data:www-data ./static/ kultmost:/var/www/static/"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -698,7 +730,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!echo $(git branch --show-current)"
|
"!echo $(git branch --show-current)"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -712,7 +745,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!rsync -avh --exclude-from=.rsyncignore --exclude=.venv/ . kultmost:/app/$(git branch --show-current)/"
|
"!rsync -avh --exclude-from=.rsyncignore --exclude=.venv/ . kultmost:/app/$(git branch --show-current)/"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -750,7 +784,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now production.service && systemctl status --no-pager -l production.service'\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",
|
"cell_type": "code",
|
||||||
@ -780,7 +815,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
|
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -818,7 +854,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\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",
|
"cell_type": "markdown",
|
||||||
@ -854,7 +891,8 @@
|
|||||||
"[Install]\n",
|
"[Install]\n",
|
||||||
"WantedBy=default.target\n",
|
"WantedBy=default.target\n",
|
||||||
"EOF"
|
"EOF"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -884,7 +922,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\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",
|
"cell_type": "markdown",
|
||||||
@ -906,7 +945,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl restart production.service'"
|
"!ssh kultmost 'systemctl restart production.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -920,7 +960,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl status production.service'"
|
"!ssh kultmost 'systemctl status production.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -934,7 +975,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl restart fastapi.service'"
|
"!ssh kultmost 'systemctl restart fastapi.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -948,7 +990,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl status fastapi.service'"
|
"!ssh kultmost 'systemctl status fastapi.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -986,7 +1029,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now vk-celery.service && systemctl status --no-pager -l vk-celery.service'\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",
|
"cell_type": "code",
|
||||||
@ -1000,7 +1044,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl restart vk-celery.service'"
|
"!ssh kultmost 'systemctl restart vk-celery.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1014,7 +1059,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl status vk-celery.service'"
|
"!ssh kultmost 'systemctl status vk-celery.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1044,7 +1090,8 @@
|
|||||||
"EOF\n",
|
"EOF\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now email-celery.service && systemctl status --no-pager -l email-celery.service'\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",
|
"cell_type": "code",
|
||||||
@ -1058,7 +1105,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl restart email-celery.service'"
|
"!ssh kultmost 'systemctl restart email-celery.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1072,7 +1120,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'systemctl status email-celery.service'"
|
"!ssh kultmost 'systemctl status email-celery.service'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -1094,7 +1143,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# !scp kultmost:/etc/nginx/sites-enabled/default nginx_conf "
|
"# !scp kultmost:/etc/nginx/sites-enabled/default nginx_conf "
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1110,7 +1160,8 @@
|
|||||||
"!scp nginx_conf kultmost:/tmp/default\n",
|
"!scp nginx_conf kultmost:/tmp/default\n",
|
||||||
"!ssh kultmost 'sudo mv /tmp/default /etc/nginx/sites-enabled/default'\n",
|
"!ssh kultmost 'sudo mv /tmp/default /etc/nginx/sites-enabled/default'\n",
|
||||||
"!ssh kultmost 'nginx -t'"
|
"!ssh kultmost 'nginx -t'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1124,7 +1175,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!ssh kultmost 'nginx -s reload'"
|
"!ssh kultmost 'nginx -s reload'"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -1149,7 +1201,8 @@
|
|||||||
"sudo usermod -a -G microk8s $USER\n",
|
"sudo usermod -a -G microk8s $USER\n",
|
||||||
"newgrp microk8s\n",
|
"newgrp microk8s\n",
|
||||||
"microk8s status"
|
"microk8s status"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -1165,7 +1218,8 @@
|
|||||||
"curl -LO https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg\n",
|
"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",
|
"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"
|
"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",
|
"cell_type": "markdown",
|
||||||
@ -1186,6 +1240,16 @@
|
|||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
|
"// MIT License\n",
|
||||||
|
"\n",
|
||||||
|
"// Copyright (c) 2026 vladislav\n",
|
||||||
|
"\n",
|
||||||
|
"// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n",
|
||||||
|
"\n",
|
||||||
|
"// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n",
|
||||||
|
"\n",
|
||||||
|
"// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||||
|
"\n",
|
||||||
"m1 = figma.currentPage.children[0]\n",
|
"m1 = figma.currentPage.children[0]\n",
|
||||||
"HTML_TAGS = new Set(['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr']);\n",
|
"HTML_TAGS = new Set(['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr']);\n",
|
||||||
"\n",
|
"\n",
|
||||||
@ -1386,7 +1450,8 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"sss = await element_create(pc, tablet, mob)\n",
|
"sss = await element_create(pc, tablet, mob)\n",
|
||||||
"sss.innerHTML"
|
"sss.innerHTML"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
@ -17,55 +17,68 @@ async def send_delivery(
|
|||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Создаёт новый документ отправки с обновлёнными статусом и полями.
|
Массовая отправка: создаёт новый документ для каждого выбранного item.
|
||||||
"""
|
"""
|
||||||
|
results = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for entry in payload.items:
|
||||||
# Проверяем, что текущий пользователь — отправитель
|
# Проверяем, что текущий пользователь — отправитель
|
||||||
if str(current_user["_id"]) != payload.sender:
|
if str(current_user["_id"]) != entry.sender:
|
||||||
raise HTTPException(status_code=403, detail="Не авторизован как отправитель")
|
errors.append({"item": entry.item, "error": "Не авторизован как отправитель"})
|
||||||
|
continue
|
||||||
|
|
||||||
# Валидация delivery_id
|
# Валидация delivery_id
|
||||||
try:
|
try:
|
||||||
delivery_id = ObjectId(payload.item)
|
delivery_id = ObjectId(entry.item)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
errors.append({"item": entry.item, "error": "Неверный delivery ID"})
|
||||||
|
continue
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
"status": "Ожидает получения",
|
"status": "Ожидает получения",
|
||||||
"delivery_type": payload.delivery_type,
|
"delivery_type": payload.delivery_type,
|
||||||
"sent_at": datetime.utcnow(),
|
"sent_at": datetime.utcnow(),
|
||||||
"updated_at": datetime.utcnow()
|
"updated_at": datetime.utcnow(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем трек только для СДЭК (если передан)
|
# Добавляем трек только для СДЭК
|
||||||
if payload.delivery_type == "СДЭК" and payload.track:
|
if payload.delivery_type == "СДЭК" and payload.track:
|
||||||
update_data["track"] = payload.track
|
update_data["track"] = payload.track
|
||||||
|
|
||||||
# Ищем существующую запись, чтобы создать новую на её основе
|
# Ищем существующую запись
|
||||||
existing = await delivery_collection.find_one(
|
existing = await delivery_collection.find_one(
|
||||||
{"_id": delivery_id, "sender._id": payload.sender},
|
{"_id": delivery_id, "sender._id": entry.sender},
|
||||||
{"_id": 0}
|
{"_id": 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
errors.append({"item": entry.item, "error": "Доставка не найдена или не для вас"})
|
||||||
|
continue
|
||||||
|
|
||||||
# Собираем новый документ для вставки
|
# Уведомление получателю
|
||||||
notify = await notify_in_profile(NotifyItem(
|
notify = await notify_in_profile(NotifyItem(
|
||||||
user_id=str(existing['receiver']['_id']),
|
user_id=str(existing["receiver"]["_id"]),
|
||||||
type_item='delivery',
|
type_item="delivery",
|
||||||
title='Доставка картин',
|
title="Доставка картин",
|
||||||
link=f'/profile/delivery',
|
link="/profile/delivery",
|
||||||
text=f'{str(existing['sender']['username'])} — прислал вам картины.'
|
text=f"{str(existing['sender']['username'])} — прислал вам картины."
|
||||||
))
|
))
|
||||||
new_doc = {**existing, **update_data, 'notify':notify.upserted_id}
|
|
||||||
|
|
||||||
# Вставляем новый документ
|
new_doc = {**existing, **update_data, "notify": notify.upserted_id}
|
||||||
|
|
||||||
result = await delivery_collection.insert_one(new_doc)
|
result = await delivery_collection.insert_one(new_doc)
|
||||||
if (iserted_id:=result.inserted_id):
|
if result.inserted_id:
|
||||||
pass
|
# TODO: send email
|
||||||
# send email
|
results.append({
|
||||||
|
"item": entry.item,
|
||||||
|
"delivery_id": str(result.inserted_id)
|
||||||
|
})
|
||||||
|
|
||||||
return {"status": "success", "delivery_id": str(iserted_id)}
|
return {
|
||||||
|
"status": "success" if not errors else "partial",
|
||||||
|
"processed": results,
|
||||||
|
"errors": errors
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/accept")
|
@router.post("/accept")
|
||||||
@ -74,28 +87,35 @@ async def accept_delivery(
|
|||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Создаёт новый документ отправки со статусом 'Получено'.
|
Массовое получение: создаёт новый документ со статусом 'Получено' для каждого item.
|
||||||
"""
|
"""
|
||||||
try:
|
results = []
|
||||||
delivery_id = ObjectId(payload.item)
|
errors = []
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
for item_id in payload.items:
|
||||||
|
try:
|
||||||
|
delivery_id = ObjectId(item_id)
|
||||||
|
except Exception:
|
||||||
|
errors.append({"item": item_id, "error": "Неверный delivery ID"})
|
||||||
|
continue
|
||||||
|
|
||||||
# Берём исходный документ, чтобы вставить новый
|
|
||||||
existing = await delivery_collection.find_one(
|
existing = await delivery_collection.find_one(
|
||||||
{"_id": delivery_id, "receiver._id": current_user["_id"]},
|
{"_id": delivery_id, "receiver._id": current_user["_id"]},
|
||||||
{"_id": 0}
|
{"_id": 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
errors.append({"item": item_id, "error": "Доставка не найдена или не для вас"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Уведомление отправителю
|
||||||
notify = await notify_in_profile(NotifyItem(
|
notify = await notify_in_profile(NotifyItem(
|
||||||
user_id=str(existing['sender']['_id']),
|
user_id=str(existing["sender"]["_id"]),
|
||||||
type_item='delivery',
|
type_item="delivery",
|
||||||
title='Доставка картин',
|
title="Доставка картин",
|
||||||
link=f'/profile/delivery',
|
link="/profile/delivery",
|
||||||
text=f'Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}.'
|
text=f"Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}."
|
||||||
))
|
))
|
||||||
|
|
||||||
new_doc = {
|
new_doc = {
|
||||||
**existing,
|
**existing,
|
||||||
"status": "Получено",
|
"status": "Получено",
|
||||||
@ -106,5 +126,20 @@ async def accept_delivery(
|
|||||||
|
|
||||||
result = await delivery_collection.insert_one(new_doc)
|
result = await delivery_collection.insert_one(new_doc)
|
||||||
|
|
||||||
await notify_collection.update_one({'_id': existing['notify']}, {'$set': {'read': False}})
|
# Помечаем предыдущее уведомление как непрочитанное (если нужно)
|
||||||
return {"status": "success", "delivery_id": str(result.inserted_id)}
|
if existing.get("notify"):
|
||||||
|
await notify_collection.update_one(
|
||||||
|
{"_id": existing["notify"]},
|
||||||
|
{"$set": {"read": False}}
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"item": item_id,
|
||||||
|
"delivery_id": str(result.inserted_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success" if not errors else "partial",
|
||||||
|
"processed": results,
|
||||||
|
"errors": errors
|
||||||
|
}
|
||||||
55
main.ipynb
55
main.ipynb
@ -557,7 +557,7 @@
|
|||||||
"\n",
|
"\n",
|
||||||
" # 1) If there's an exhibition that is going to be finished soon -> move deliveries to \"Возврат художнику\"\n",
|
" # 1) If there's an exhibition that is going to be finished soon -> move deliveries to \"Возврат художнику\"\n",
|
||||||
" exhibition_finished = await exhibition_collection.find_one_and_update(\n",
|
" exhibition_finished = await exhibition_collection.find_one_and_update(\n",
|
||||||
" {**prod_filter, \"date_finish\": {\"$lt\": limit_dt}},\n",
|
" {**prod_filter, \"status\": 1, \"date_finish\": {\"$lt\": limit_dt}},\n",
|
||||||
" {\"$set\": {\"status\": 2}},\n",
|
" {\"$set\": {\"status\": 2}},\n",
|
||||||
" return_document=ReturnDocument.AFTER,\n",
|
" return_document=ReturnDocument.AFTER,\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
@ -2903,7 +2903,7 @@
|
|||||||
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
|
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
|
||||||
" data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)\n",
|
" data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request, name=\"form/competition_edit.html\", context={\"competition\": str(data), \"user\": user}\n",
|
" request=request, name=\"form/competition_edit.html\", context={\"competition\": str(data), \"competition_id\": competition_id, \"user\": user}\n",
|
||||||
" )\n"
|
" )\n"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
@ -4554,6 +4554,48 @@
|
|||||||
"id": "6157e497",
|
"id": "6157e497",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# @app.get('/profile/delivery/{type_d}/{item_id}')\n",
|
||||||
|
"# async def profile_delivery_by_old(request: Request, type_d:str, item_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",
|
||||||
|
"# f'{type_d}_id': ObjectId(item_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",
|
||||||
|
"# data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||||
|
"\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": "089ba597",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"@app.get('/profile/delivery/competition/{competition_id}')\n",
|
"@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",
|
"async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):\n",
|
||||||
@ -4564,7 +4606,6 @@
|
|||||||
" {\"user._id\": user.get('_id')},\n",
|
" {\"user._id\": user.get('_id')},\n",
|
||||||
" {\"receiver._id\": user.get('_id')}\n",
|
" {\"receiver._id\": user.get('_id')}\n",
|
||||||
" ],\n",
|
" ],\n",
|
||||||
" 'competition_id': ObjectId(competition_id),\n",
|
|
||||||
" 'prod': PROD,\n",
|
" 'prod': PROD,\n",
|
||||||
" }},\n",
|
" }},\n",
|
||||||
" {\"$sort\": {\"_id\": -1}},\n",
|
" {\"$sort\": {\"_id\": -1}},\n",
|
||||||
@ -4577,6 +4618,8 @@
|
|||||||
" ])\n",
|
" ])\n",
|
||||||
"\n",
|
"\n",
|
||||||
" data = [doc async for doc in cursor]\n",
|
" data = [doc async for doc in cursor]\n",
|
||||||
|
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||||
|
"\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request,\n",
|
" request=request,\n",
|
||||||
" name=\"profile/delivery_old.html\",\n",
|
" name=\"profile/delivery_old.html\",\n",
|
||||||
@ -4591,7 +4634,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"id": "d68edb20",
|
"id": "2148ef21",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
@ -4617,6 +4660,8 @@
|
|||||||
" ])\n",
|
" ])\n",
|
||||||
"\n",
|
"\n",
|
||||||
" data = [doc async for doc in cursor]\n",
|
" data = [doc async for doc in cursor]\n",
|
||||||
|
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||||
|
"\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request,\n",
|
" request=request,\n",
|
||||||
" name=\"profile/delivery_old.html\",\n",
|
" name=\"profile/delivery_old.html\",\n",
|
||||||
@ -4624,7 +4669,7 @@
|
|||||||
" \"user\": user,\n",
|
" \"user\": user,\n",
|
||||||
" \"data\": data,\n",
|
" \"data\": data,\n",
|
||||||
" }\n",
|
" }\n",
|
||||||
" )\n"
|
" )"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
|
|||||||
44
main.py
44
main.py
@ -345,7 +345,7 @@ async def cron_every_five_min(token: str = Security(get_token)):
|
|||||||
|
|
||||||
# 1) If there's an exhibition that is going to be finished soon -> move deliveries to "Возврат художнику"
|
# 1) If there's an exhibition that is going to be finished soon -> move deliveries to "Возврат художнику"
|
||||||
exhibition_finished = await exhibition_collection.find_one_and_update(
|
exhibition_finished = await exhibition_collection.find_one_and_update(
|
||||||
{**prod_filter, "date_finish": {"$lt": limit_dt}},
|
{**prod_filter, "status": 1, "date_finish": {"$lt": limit_dt}},
|
||||||
{"$set": {"status": 2}},
|
{"$set": {"status": 2}},
|
||||||
return_document=ReturnDocument.AFTER,
|
return_document=ReturnDocument.AFTER,
|
||||||
)
|
)
|
||||||
@ -2162,7 +2162,7 @@ async def form_(
|
|||||||
competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})
|
competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})
|
||||||
data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)
|
data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request, name="form/competition_edit.html", context={"competition": str(data), "user": user}
|
request=request, name="form/competition_edit.html", context={"competition": str(data), "competition_id": competition_id, "user": user}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -3459,6 +3459,40 @@ async def profile_delivery_history_post(request: Request, post_id:str, user: Dic
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# @app.get('/profile/delivery/{type_d}/{item_id}')
|
||||||
|
# async def profile_delivery_by_old(request: Request, type_d:str, item_id:str, user: Dict = Security(get_current_user)):
|
||||||
|
# cursor = delivery_collection.aggregate([
|
||||||
|
# {"$match": {
|
||||||
|
# "$or": [
|
||||||
|
# {"sender._id": user.get('_id')},
|
||||||
|
# {"user._id": user.get('_id')},
|
||||||
|
# {"receiver._id": user.get('_id')}
|
||||||
|
# ],
|
||||||
|
# f'{type_d}_id': ObjectId(item_id),
|
||||||
|
# 'prod': PROD,
|
||||||
|
# }},
|
||||||
|
# {"$sort": {"_id": -1}},
|
||||||
|
# {"$group": {
|
||||||
|
# "_id": "$post_id",
|
||||||
|
# "doc": {"$first": "$$ROOT"}
|
||||||
|
# }},
|
||||||
|
# {"$replaceRoot": {"newRoot": "$doc"}},
|
||||||
|
# {"$sort": {"_id": -1}},
|
||||||
|
# ])
|
||||||
|
|
||||||
|
# data = [doc async for doc in cursor]
|
||||||
|
# data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="profile/delivery_old.html",
|
||||||
|
# context={
|
||||||
|
# "user": user,
|
||||||
|
# "data": data,
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
@app.get('/profile/delivery/competition/{competition_id}')
|
@app.get('/profile/delivery/competition/{competition_id}')
|
||||||
async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):
|
async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):
|
||||||
cursor = delivery_collection.aggregate([
|
cursor = delivery_collection.aggregate([
|
||||||
@ -3468,7 +3502,6 @@ async def profile_delivery_by_competition(request: Request, competition_id:str,
|
|||||||
{"user._id": user.get('_id')},
|
{"user._id": user.get('_id')},
|
||||||
{"receiver._id": user.get('_id')}
|
{"receiver._id": user.get('_id')}
|
||||||
],
|
],
|
||||||
'competition_id': ObjectId(competition_id),
|
|
||||||
'prod': PROD,
|
'prod': PROD,
|
||||||
}},
|
}},
|
||||||
{"$sort": {"_id": -1}},
|
{"$sort": {"_id": -1}},
|
||||||
@ -3481,6 +3514,8 @@ async def profile_delivery_by_competition(request: Request, competition_id:str,
|
|||||||
])
|
])
|
||||||
|
|
||||||
data = [doc async for doc in cursor]
|
data = [doc async for doc in cursor]
|
||||||
|
data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="profile/delivery_old.html",
|
name="profile/delivery_old.html",
|
||||||
@ -3513,6 +3548,8 @@ async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, us
|
|||||||
])
|
])
|
||||||
|
|
||||||
data = [doc async for doc in cursor]
|
data = [doc async for doc in cursor]
|
||||||
|
data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="profile/delivery_old.html",
|
name="profile/delivery_old.html",
|
||||||
@ -3522,7 +3559,6 @@ async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, us
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/profile/delivery",
|
"/profile/delivery",
|
||||||
response_class=HTMLResponse,
|
response_class=HTMLResponse,
|
||||||
|
|||||||
10
models.py
10
models.py
@ -14,16 +14,20 @@ class NotifyItem(BaseModel):
|
|||||||
type_item: str = "c"
|
type_item: str = "c"
|
||||||
|
|
||||||
|
|
||||||
class DeliverySendRequest(BaseModel):
|
class DeliverySendItem(BaseModel):
|
||||||
|
item: str
|
||||||
sender: str
|
sender: str
|
||||||
too: str
|
too: str
|
||||||
item: str
|
|
||||||
|
|
||||||
|
class DeliverySendRequest(BaseModel):
|
||||||
|
items: List[DeliverySendItem]
|
||||||
delivery_type: Literal["СДЭК", "Самостоятельно"]
|
delivery_type: Literal["СДЭК", "Самостоятельно"]
|
||||||
track: Optional[str] = Field(None, description="Трэк-номер для СДЭК")
|
track: Optional[str] = Field(None, description="Трэк-номер для СДЭК")
|
||||||
|
|
||||||
|
|
||||||
class DeliveryAcceptRequest(BaseModel):
|
class DeliveryAcceptRequest(BaseModel):
|
||||||
item: str
|
items: List[str]
|
||||||
|
|
||||||
|
|
||||||
class UserRegister(BaseModel):
|
class UserRegister(BaseModel):
|
||||||
|
|||||||
@ -712,6 +712,7 @@
|
|||||||
" },{\n",
|
" },{\n",
|
||||||
" \"$set\":{\n",
|
" \"$set\":{\n",
|
||||||
" \"sender\": user,\n",
|
" \"sender\": user,\n",
|
||||||
|
" \"user\": user,\n",
|
||||||
" \"receiver\": display_user(exhibition_owner),\n",
|
" \"receiver\": display_user(exhibition_owner),\n",
|
||||||
" \"exhibition_id\": str(exhibition.get('_id')),\n",
|
" \"exhibition_id\": str(exhibition.get('_id')),\n",
|
||||||
" \"address\": exhibition.get('address'),\n",
|
" \"address\": exhibition.get('address'),\n",
|
||||||
|
|||||||
1
tasks.py
1
tasks.py
@ -541,6 +541,7 @@ async def exhibition_send_invitations(exhibition_id):
|
|||||||
},{
|
},{
|
||||||
"$set":{
|
"$set":{
|
||||||
"sender": user,
|
"sender": user,
|
||||||
|
"user": user,
|
||||||
"receiver": display_user(exhibition_owner),
|
"receiver": display_user(exhibition_owner),
|
||||||
"exhibition_id": str(exhibition.get('_id')),
|
"exhibition_id": str(exhibition.get('_id')),
|
||||||
"address": exhibition.get('address'),
|
"address": exhibition.get('address'),
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
|
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span class="b_sdf">До {{ (competition.date_finish.strftime('%d.%m.%Y')) if competition.date_finish else ''}}</span>
|
||||||
<header class="c_m876y b_9xtz9">
|
<header class="c_m876y b_9xtz9">
|
||||||
<h3 class="c_hysjg b_8f3uh">{{ competition.title }}</h3>
|
<h3 class="c_hysjg b_8f3uh">{{ competition.title }}</h3>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{% import "competition/list.html" as competition %}
|
{% import "competition/list.html" as competition %}
|
||||||
{% import "components/checkbox.html" as checkbox %}
|
{% import "components/checkbox.html" as checkbox %}
|
||||||
|
|
||||||
{% macro section(user, competition) -%}
|
{% macro section(user, competition, competition_id) -%}
|
||||||
<form class="c_sbrxk">
|
<form class="c_sbrxk">
|
||||||
<div class="c_dtjoh">
|
<div class="c_dtjoh">
|
||||||
<h3>Выбор</h3>
|
<h3>Выбор</h3>
|
||||||
@ -346,7 +346,7 @@ function bindLinkValidation({
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const competition = JSON.parse({{competition|tojson}})
|
const competition = JSON.parse({{competition|tojson}})
|
||||||
const competitionId = competition._id
|
const competitionId = `{{competition_id}}`
|
||||||
|
|
||||||
document.getElementById('title').value = competition.title;
|
document.getElementById('title').value = competition.title;
|
||||||
document.getElementById('preview_title').textContent = competition.title;
|
document.getElementById('preview_title').textContent = competition.title;
|
||||||
@ -595,7 +595,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
console.log('Отправка формы...');
|
console.log('Отправка формы...');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/competition/${competitionId}`, {
|
const res = await fetch(`/api/competition/{{competition_id}}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
@ -895,5 +895,4 @@ input[type="number"]:focus::-webkit-inner-spin-button {display: none;}
|
|||||||
.c_sbrxk{z-index: 1000;}
|
.c_sbrxk{z-index: 1000;}
|
||||||
#preview_poster{width: 335px;}
|
#preview_poster{width: 335px;}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
@ -14,6 +14,6 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header.section(user) }}
|
{{ header.section(user) }}
|
||||||
{{ form.section(user, competition) }}
|
{{ form.section(user, competition, competition_id) }}
|
||||||
{{ footer.section() }}
|
{{ footer.section() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
{% block title %}Где картины{% endblock %}
|
{% block title %}Где картины{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_doc(doc) %}
|
{% 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}}">
|
<figure class="c_4b1hy b_uu2ub" data-exhibition_id="{{doc.get('exhibition_id')}}" data-delivery-id="{{doc._id}}" data-sender="{{doc.sender._id}}" data-receiver="{{doc.receiver._id}}">
|
||||||
<img class="b_1fvkt" src="/b/posts/{{doc.img}}">
|
<img class="b_1fvkt" src="/b/posts/{{doc.img}}">
|
||||||
@ -29,8 +28,10 @@
|
|||||||
<span class="b_5dr47_2">{{doc.status}}</span>
|
<span class="b_5dr47_2">{{doc.status}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
|
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
|
||||||
<h2 class="c_9hvtd b_f5fb2">{{doc.title}}</h2>
|
<h2 class="c_9hvtd b_f5fb2">{{doc.title}}</h2>
|
||||||
|
|
||||||
{% if doc.type == 'exhibition' and doc.get('exhibition') %}
|
{% if doc.type == 'exhibition' and doc.get('exhibition') %}
|
||||||
<p class="b_6spaf">{{doc.get('exhibition').title}}</p>
|
<p class="b_6spaf">{{doc.get('exhibition').title}}</p>
|
||||||
<div class="b_xz6em">
|
<div class="b_xz6em">
|
||||||
@ -53,6 +54,7 @@
|
|||||||
{% if doc.track %}
|
{% if doc.track %}
|
||||||
<p class="b_6spaf">Трэк-номер: {{doc.track}}</p>
|
<p class="b_6spaf">Трэк-номер: {{doc.track}}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if doc.sender %}
|
{% if doc.sender %}
|
||||||
<div class="b_xz6em">
|
<div class="b_xz6em">
|
||||||
<img src="/a/{{doc.sender._id}}" class="b_ob59y">
|
<img src="/a/{{doc.sender._id}}" class="b_ob59y">
|
||||||
@ -62,6 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if doc.receiver %}
|
{% if doc.receiver %}
|
||||||
<div class="b_xz6em">
|
<div class="b_xz6em">
|
||||||
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
|
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
|
||||||
@ -72,17 +75,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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}}')">
|
{# Checkbox instead of per-item button #}
|
||||||
<span class="b_bvl0d">Отправить</span>
|
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id %}
|
||||||
</button>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="offer-checkbox delivery-checkbox send-checkbox"
|
||||||
|
data-id="{{doc._id}}"
|
||||||
|
data-sender="{{doc.sender._id}}"
|
||||||
|
data-receiver="{{doc.receiver._id}}">
|
||||||
|
<span>Выбрать для отправки</span>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
|
|
||||||
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
|
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id %}
|
||||||
<span class="b_bvl0d">Получить</span>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
</button>
|
<input type="checkbox"
|
||||||
|
class="offer-checkbox delivery-checkbox accept-checkbox"
|
||||||
|
data-id="{{doc._id}}">
|
||||||
|
<span>Выбрать для получения</span>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="b_6spaf">
|
<p class="b_6spaf">
|
||||||
{{ (doc._id.generation_time.strftime('%d.%m.%Y')) if doc._id else ''}}
|
{{ (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>
|
<b>{{ '%02d' % ((doc._id.generation_time.hour + 3) % 24) if doc._id else '' }}:{{ '%02d' % doc._id.generation_time.minute if doc._id else '' }}</b>
|
||||||
@ -91,18 +106,18 @@
|
|||||||
</figure>
|
</figure>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header.section(user) }}
|
{{ header.section(user) }}
|
||||||
<section class="c_dg3k4">
|
<section class="c_dg3k4">
|
||||||
<div class="c_gtoe9">
|
<div class="c_gtoe9">
|
||||||
{{ profile_card.profile_card(user) }}
|
{{ profile_card.profile_card(user) }}
|
||||||
<div class="c_ru432">
|
<div class="c_ru432">
|
||||||
{{ profile_nav.profile_nav(user, 'delivery') }}
|
{{ profile_nav.profile_nav(user, 'delivery') }}
|
||||||
<div class="c_tihtk" id="main">
|
<div class="c_tihtk" id="main">
|
||||||
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
|
|
||||||
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
|
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
|
||||||
<article class="c_9fgdh b_ax8vz">
|
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
|
||||||
|
<article class="c_9fgdh b_ax8vz">
|
||||||
<div class="c_z3dpx b_73p10">
|
<div class="c_z3dpx b_73p10">
|
||||||
<header class="c_m876y b_9xtz9">
|
<header class="c_m876y b_9xtz9">
|
||||||
<h3 class="b_8f3uh">Настройки отправки</h3>
|
<h3 class="b_8f3uh">Настройки отправки</h3>
|
||||||
@ -110,28 +125,300 @@
|
|||||||
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
|
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
|
||||||
<div class="b_73p10">
|
<div class="b_73p10">
|
||||||
<div class="c_aihm6 floating-group">
|
<div class="c_aihm6 floating-group">
|
||||||
<select class="c_xhd5k" type="text" id="delivery_type" name="delivery_type" required>
|
<select class="c_xhd5k" id="delivery_type" name="delivery_type" required>
|
||||||
<option>СДЭК</option>
|
<option>СДЭК</option>
|
||||||
<option>Самостоятельно</option>
|
<option>Самостоятельно</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="c_aihm6 floating-group">
|
<div class="c_aihm6 floating-group" id="track-group">
|
||||||
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
||||||
data-toast="Поле Трэк-номер должно быть заполнено">
|
data-toast="Поле Трэк-номер должно быть заполнено">
|
||||||
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for doc in data if doc.status in statuses %}
|
|
||||||
{{ render_doc(doc) }}
|
{% for doc in data %}
|
||||||
{% endfor %}
|
{{ render_doc(doc) }}
|
||||||
{% for doc in data if doc.status not in statuses %}
|
{% endfor %}
|
||||||
{{ render_doc(doc) }}
|
|
||||||
{% endfor %}
|
{# Sticky bottom action bar #}
|
||||||
|
<div class="st" style="z-index: 2000;min-height: 101px;padding: 25px;gap: 5px;">
|
||||||
|
<button class="b_wy3rt ba" id="btnSendSelected" style="display: none;" onclick="btnSendSelected()">
|
||||||
|
<span class="b_bvl0d">Отправить выбранные</span>
|
||||||
|
</button>
|
||||||
|
<button class="b_wy3rt ba" id="btnAcceptSelected" style="display: none;" onclick="btnAcceptSelected()">
|
||||||
|
<span class="b_bvl0d">Получить выбранные</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helpers: collect selected IDs
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function getSelectedSendIds() {
|
||||||
|
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
||||||
|
id: cb.dataset.id,
|
||||||
|
sender: cb.dataset.sender,
|
||||||
|
receiver: cb.dataset.receiver
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedAcceptIds() {
|
||||||
|
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Sticky buttons visibility
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function updateStickyButtons() {
|
||||||
|
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
|
||||||
|
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||||
|
|
||||||
|
const sendBtn = document.getElementById('btnSendSelected');
|
||||||
|
const acceptBtn = document.getElementById('btnAcceptSelected');
|
||||||
|
|
||||||
|
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
|
||||||
|
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Show / hide track field depending on delivery_type
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function toggleTrackField() {
|
||||||
|
const deliveryType = document.getElementById('delivery_type');
|
||||||
|
const trackGroup = document.getElementById('track-group');
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
|
||||||
|
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||||
|
|
||||||
|
if (deliveryType.value === 'Самостоятельно') {
|
||||||
|
trackGroup.style.display = 'none';
|
||||||
|
trackInput.removeAttribute('required');
|
||||||
|
trackInput.value = '';
|
||||||
|
} else {
|
||||||
|
trackGroup.style.display = '';
|
||||||
|
trackInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Validation helper (used by Send)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function validateRequiredFields() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Bulk Accept
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
async function btnAcceptSelected() {
|
||||||
|
const ids = getSelectedAcceptIds();
|
||||||
|
if (!ids.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/delivery/accept', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ items: ids })
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Update UI for successfully processed items
|
||||||
|
(data.processed || []).forEach(({ item }) => {
|
||||||
|
const checkbox = document.querySelector(`.accept-checkbox[data-id="${item}"]`);
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
const figcaption = checkbox.closest('figcaption');
|
||||||
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
|
const statusSpan = statusDiv.querySelector('span');
|
||||||
|
|
||||||
|
statusDiv.className = 'b_thlt1';
|
||||||
|
statusSpan.className = 'b_5dr47';
|
||||||
|
statusSpan.textContent = 'Получено';
|
||||||
|
checkbox.closest('label').style.display = 'none';
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.errors?.length) {
|
||||||
|
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Bulk Send
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
async function btnSendSelected() {
|
||||||
|
const items = getSelectedSendIds();
|
||||||
|
if (!items.length) return;
|
||||||
|
|
||||||
|
// Validate required fields first
|
||||||
|
if (!validateRequiredFields()) return;
|
||||||
|
|
||||||
|
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
let track = '';
|
||||||
|
if (delivery_type === 'СДЭК' && trackInput) {
|
||||||
|
track = trackInput.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/delivery/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: items.map(i => ({
|
||||||
|
item: i.id,
|
||||||
|
sender: i.sender,
|
||||||
|
too: i.receiver
|
||||||
|
})),
|
||||||
|
delivery_type,
|
||||||
|
track: track || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Update UI for successfully processed items
|
||||||
|
(data.processed || []).forEach(({ item }) => {
|
||||||
|
const checkbox = document.querySelector(`.send-checkbox[data-id="${item}"]`);
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
const figcaption = checkbox.closest('figcaption');
|
||||||
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
|
const statusSpan = statusDiv.querySelector('span');
|
||||||
|
|
||||||
|
statusDiv.className = 'b_thlt1_2';
|
||||||
|
statusSpan.className = 'b_5dr47_2';
|
||||||
|
statusSpan.textContent = 'Ожидает получения';
|
||||||
|
checkbox.closest('label').style.display = 'none';
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.errors?.length) {
|
||||||
|
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Event listeners
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Initial state of track field
|
||||||
|
toggleTrackField();
|
||||||
|
|
||||||
|
// Change of delivery type
|
||||||
|
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||||
|
|
||||||
|
// Checkbox changes → update sticky buttons
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('delivery-checkbox')) {
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<script defer>
|
<script defer>
|
||||||
|
function toggleTrackField() {
|
||||||
|
const deliveryType = document.getElementById('delivery_type');
|
||||||
|
const trackGroup = document.getElementById('track-group');
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
|
||||||
|
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||||
|
|
||||||
|
if (deliveryType.value === 'Самостоятельно') {
|
||||||
|
trackGroup.style.display = 'none';
|
||||||
|
trackInput.removeAttribute('required');
|
||||||
|
trackInput.value = ''; // clear value when hidden
|
||||||
|
} else {
|
||||||
|
trackGroup.style.display = '';
|
||||||
|
trackInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', toggleTrackField);
|
||||||
|
|
||||||
|
// Run every time the select changes
|
||||||
|
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||||
const query = window.location.href.split('?')[1] || '';
|
const query = window.location.href.split('?')[1] || '';
|
||||||
let exhibition_id = null;
|
let exhibition_id = null;
|
||||||
if (query) {
|
if (query) {
|
||||||
@ -175,132 +462,7 @@
|
|||||||
deliveryType.addEventListener('change', toggleTrack);
|
deliveryType.addEventListener('change', toggleTrack);
|
||||||
toggleTrack();
|
toggleTrack();
|
||||||
</script>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -71,15 +71,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
|
{# Checkbox instead of per-item button #}
|
||||||
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
|
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id %}
|
||||||
<span class="b_bvl0d">Отправить</span>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
</button>
|
<input type="checkbox"
|
||||||
|
class="offer-checkbox delivery-checkbox send-checkbox"
|
||||||
|
data-id="{{doc._id}}"
|
||||||
|
data-sender="{{doc.sender._id}}"
|
||||||
|
data-receiver="{{doc.receiver._id}}">
|
||||||
|
<span>Выбрать для отправки</span>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
|
|
||||||
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
|
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id %}
|
||||||
<span class="b_bvl0d">Получить</span>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
</button>
|
<input type="checkbox"
|
||||||
|
class="offer-checkbox delivery-checkbox accept-checkbox"
|
||||||
|
data-id="{{doc._id}}">
|
||||||
|
<span>Выбрать для получения</span>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="b_6spaf">
|
<p class="b_6spaf">
|
||||||
@ -189,7 +199,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="c_aihm6 floating-group">
|
<div class="c_aihm6 floating-group" id="track-group">
|
||||||
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
||||||
data-toast="Поле Трэк-номер должно быть заполнено">
|
data-toast="Поле Трэк-номер должно быть заполнено">
|
||||||
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
||||||
@ -205,7 +215,40 @@
|
|||||||
{% for doc in data if doc.status not in statuses %}
|
{% for doc in data if doc.status not in statuses %}
|
||||||
{{ render_doc(doc) }}
|
{{ render_doc(doc) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<script defer>
|
{# Sticky bottom action bar #}
|
||||||
|
<div class="st" style="z-index: 2000;min-height: 101px;padding: 25px;gap: 5px;">
|
||||||
|
<button class="b_wy3rt ba" id="btnSendSelected" style="display: none;" onclick="btnSendSelected()">
|
||||||
|
<span class="b_bvl0d">Отправить выбранные</span>
|
||||||
|
</button>
|
||||||
|
<button class="b_wy3rt ba" id="btnAcceptSelected" style="display: none;" onclick="btnAcceptSelected()">
|
||||||
|
<span class="b_bvl0d">Получить выбранные</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function toggleTrackField() {
|
||||||
|
const deliveryType = document.getElementById('delivery_type');
|
||||||
|
const trackGroup = document.getElementById('track-group');
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
|
||||||
|
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||||
|
|
||||||
|
if (deliveryType.value === 'Самостоятельно') {
|
||||||
|
trackGroup.style.display = 'none';
|
||||||
|
trackInput.removeAttribute('required');
|
||||||
|
trackInput.value = ''; // clear value when hidden
|
||||||
|
} else {
|
||||||
|
trackGroup.style.display = '';
|
||||||
|
trackInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', toggleTrackField);
|
||||||
|
|
||||||
|
// Run every time the select changes
|
||||||
|
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
const query = window.location.href.split('?')[1] || '';
|
const query = window.location.href.split('?')[1] || '';
|
||||||
let exhibition_id = null;
|
let exhibition_id = null;
|
||||||
if (query) {
|
if (query) {
|
||||||
@ -250,57 +293,59 @@
|
|||||||
toggleTrack();
|
toggleTrack();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
async function btnAccept(btn, itemId){
|
// ─────────────────────────────────────────────
|
||||||
const figcaption = btn.parentElement;
|
// Helpers: collect selected IDs
|
||||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
// ─────────────────────────────────────────────
|
||||||
const statusSpan = statusDiv.querySelector('span')
|
function getSelectedSendIds() {
|
||||||
|
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
||||||
|
id: cb.dataset.id,
|
||||||
|
sender: cb.dataset.sender,
|
||||||
|
receiver: cb.dataset.receiver
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function getSelectedAcceptIds() {
|
||||||
const res = await fetch('/api/delivery/accept', {
|
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
||||||
method: 'POST',
|
}
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
// ─────────────────────────────────────────────
|
||||||
let errorMessage = 'Ошибка сохранения';
|
// Sticky buttons visibility
|
||||||
try {
|
// ─────────────────────────────────────────────
|
||||||
const errorData = await res.json();
|
function updateStickyButtons() {
|
||||||
errorMessage = errorData.detail || errorMessage;
|
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
|
||||||
} catch (e) {
|
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||||
try {
|
|
||||||
const errorText = await res.text();
|
const sendBtn = document.getElementById('btnSendSelected');
|
||||||
errorMessage = errorText || errorMessage;
|
const acceptBtn = document.getElementById('btnAcceptSelected');
|
||||||
} catch (e) {
|
|
||||||
|
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
|
||||||
|
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Show / hide track field depending on delivery_type
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function toggleTrackField() {
|
||||||
|
const deliveryType = document.getElementById('delivery_type');
|
||||||
|
const trackGroup = document.getElementById('track-group');
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
|
||||||
|
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||||
|
|
||||||
|
if (deliveryType.value === 'Самостоятельно') {
|
||||||
|
trackGroup.style.display = 'none';
|
||||||
|
trackInput.removeAttribute('required');
|
||||||
|
trackInput.value = '';
|
||||||
|
} else {
|
||||||
|
trackGroup.style.display = '';
|
||||||
|
trackInput.setAttribute('required', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
// Validation helper (used by Send)
|
||||||
const trackValue = document.getElementById('track');
|
// ─────────────────────────────────────────────
|
||||||
|
function validateRequiredFields() {
|
||||||
let track = ''
|
|
||||||
if (delivery_type === 'СДЭК' && trackValue) {
|
|
||||||
track = trackValue.value
|
|
||||||
}
|
|
||||||
const requiredInputs = document.querySelectorAll('input[required]');
|
const requiredInputs = document.querySelectorAll('input[required]');
|
||||||
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
||||||
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
||||||
@ -310,7 +355,7 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
|
|
||||||
requiredFields.forEach(field => {
|
requiredFields.forEach(field => {
|
||||||
const group = field.closest('.floating-group');
|
const group = field.closest('.floating-group');
|
||||||
if (!group) return; // skip if no group wrapper
|
if (!group) return;
|
||||||
|
|
||||||
let isEmpty = false;
|
let isEmpty = false;
|
||||||
if (field.type === 'file') {
|
if (field.type === 'file') {
|
||||||
@ -322,7 +367,7 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
group.classList.add('error');
|
group.classList.add('error');
|
||||||
hasError = true;
|
hasError = true;
|
||||||
showToast({message: field.dataset.toast, duration:2500});
|
showToast({ message: field.dataset.toast || 'Заполните обязательное поле', duration: 2500 });
|
||||||
if (!firstErrorField) firstErrorField = field;
|
if (!firstErrorField) firstErrorField = field;
|
||||||
} else {
|
} else {
|
||||||
group.classList.remove('error');
|
group.classList.remove('error');
|
||||||
@ -332,19 +377,98 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
if (hasError && firstErrorField) {
|
if (hasError && firstErrorField) {
|
||||||
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
setTimeout(() => firstErrorField.focus(), 600);
|
setTimeout(() => firstErrorField.focus(), 600);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Bulk Accept
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
async function btnAcceptSelected() {
|
||||||
|
const ids = getSelectedAcceptIds();
|
||||||
|
if (!ids.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/delivery/accept', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ items: ids })
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Update UI for successfully processed items
|
||||||
|
(data.processed || []).forEach(({ item }) => {
|
||||||
|
const checkbox = document.querySelector(`.accept-checkbox[data-id="${item}"]`);
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
const figcaption = checkbox.closest('figcaption');
|
||||||
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
|
const statusSpan = statusDiv.querySelector('span');
|
||||||
|
|
||||||
|
statusDiv.className = 'b_thlt1';
|
||||||
|
statusSpan.className = 'b_5dr47';
|
||||||
|
statusSpan.textContent = 'Получено';
|
||||||
|
checkbox.closest('label').style.display = 'none';
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.errors?.length) {
|
||||||
|
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Bulk Send
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
async function btnSendSelected() {
|
||||||
|
const items = getSelectedSendIds();
|
||||||
|
if (!items.length) return;
|
||||||
|
|
||||||
|
// Validate required fields first
|
||||||
|
if (!validateRequiredFields()) return;
|
||||||
|
|
||||||
|
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
||||||
|
const trackInput = document.getElementById('track');
|
||||||
|
let track = '';
|
||||||
|
if (delivery_type === 'СДЭК' && trackInput) {
|
||||||
|
track = trackInput.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/delivery/send', {
|
const res = await fetch('/api/delivery/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sender,
|
items: items.map(i => ({
|
||||||
too,
|
item: i.id,
|
||||||
item: itemId,
|
sender: i.sender,
|
||||||
|
too: i.receiver
|
||||||
|
})),
|
||||||
delivery_type,
|
delivery_type,
|
||||||
track: track
|
track: track || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -354,26 +478,61 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
const errorData = await res.json();
|
const errorData = await res.json();
|
||||||
errorMessage = errorData.detail || errorMessage;
|
errorMessage = errorData.detail || errorMessage;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If response isn't JSON, try to get text
|
|
||||||
try {
|
try {
|
||||||
const errorText = await res.text();
|
const errorText = await res.text();
|
||||||
errorMessage = errorText || errorMessage;
|
errorMessage = errorText || errorMessage;
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// Fallback to generic error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
showToast({ message: errorMessage, duration: 2500 });
|
showToast({ message: errorMessage, duration: 2500 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Update UI for successfully processed items
|
||||||
|
(data.processed || []).forEach(({ item }) => {
|
||||||
|
const checkbox = document.querySelector(`.send-checkbox[data-id="${item}"]`);
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
const figcaption = checkbox.closest('figcaption');
|
||||||
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
|
const statusSpan = statusDiv.querySelector('span');
|
||||||
|
|
||||||
statusDiv.className = 'b_thlt1_2';
|
statusDiv.className = 'b_thlt1_2';
|
||||||
statusSpan.className = 'b_5dr47_2';
|
statusSpan.className = 'b_5dr47_2';
|
||||||
statusSpan.textContent = 'Ожидает получения';
|
statusSpan.textContent = 'Ожидает получения';
|
||||||
btn.style.display = 'none'
|
checkbox.closest('label').style.display = 'none';
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.errors?.length) {
|
||||||
|
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Network error:', error);
|
console.error('Network error:', error);
|
||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Event listeners
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Initial state of track field
|
||||||
|
toggleTrackField();
|
||||||
|
|
||||||
|
// Change of delivery type
|
||||||
|
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||||
|
|
||||||
|
// Checkbox changes → update sticky buttons
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('delivery-checkbox')) {
|
||||||
|
updateStickyButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="c_ru432">
|
<div class="c_ru432">
|
||||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||||
<div class="c_3ihvi" id="main">
|
<div class="c_3ihvi" id="main">
|
||||||
<a class="c_ctfod b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
||||||
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
|
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
<input type="text" id="filter-author" name="filter-author" class="c_xhd5k">
|
<input type="text" id="filter-author" name="filter-author" class="c_xhd5k">
|
||||||
<label for="filter-author" class="c_z3ybu floating-label">Автор (ФИО)</label>
|
<label for="filter-author" class="c_z3ybu floating-label">Автор (ФИО)</label>
|
||||||
</div>
|
</div>
|
||||||
<button id="apply-filters" class="btn b">Применить фильтры</button>
|
<button id="apply-filters" class="btn b ba">Применить фильтры</button>
|
||||||
</div>
|
</div>
|
||||||
<article class="c_9fgdh b_ax8vz">
|
<article class="c_9fgdh b_ax8vz">
|
||||||
<!-- Poster -->
|
<!-- Poster -->
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||||
<div class="c_3ihvi" id="main">
|
<div class="c_3ihvi" id="main">
|
||||||
{% if now < exhibition.date_finish %}
|
{% if now < exhibition.date_finish %}
|
||||||
<a class="b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
<a class="b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||||
<span class="c_d2l0b b_al8e7">Каталог картин</span>
|
<span class="c_d2l0b b_al8e7">Каталог картин</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
%}
|
%}
|
||||||
{% set uniq_count = ids | unique | list | length %}
|
{% set uniq_count = ids | unique | list | length %}
|
||||||
{% if uniq_count > 0 %}
|
{% if uniq_count > 0 %}
|
||||||
<button class="b_289i1" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
<button class="b_289i1 ba" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||||
<span class="c_d2l0b b_al8e7">Отправить приглашени{% if uniq_count > 1 %}я{% else %}е{% endif %}</span>
|
<span class="c_d2l0b b_al8e7">Отправить приглашени{% if uniq_count > 1 %}я{% else %}е{% endif %}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -98,9 +98,10 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.status == 'Ожидает получения' %}
|
{% if post.status == 'Ожидает получения' %}
|
||||||
<button class="b_289i1" onclick="btnAccept(this, '{{post.d_id}}')">
|
{# <button class="b_289i1" onclick="btnAccept(this, '{{post.d_id}}')"><span class="b_al8e7">Получить</span></button> #}
|
||||||
|
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
||||||
<span class="b_al8e7">Получить</span>
|
<span class="b_al8e7">Получить</span>
|
||||||
</button>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.status in ['Возврат художнику']%}
|
{% if post.status in ['Возврат художнику']%}
|
||||||
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
||||||
@ -114,8 +115,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{#
|
||||||
<script>
|
<script>
|
||||||
async function btnAccept(btn, itemId){
|
async function btnAccept(btn, itemId){
|
||||||
const figcaption = btn.parentElement;
|
const figcaption = btn.parentElement;
|
||||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
const statusSpan = statusDiv.querySelector('span')
|
const statusSpan = statusDiv.querySelector('span')
|
||||||
@ -153,6 +155,9 @@ async function btnAccept(btn, itemId){
|
|||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
|
<script>
|
||||||
async function sendInvitations(exhibition_id) {
|
async function sendInvitations(exhibition_id) {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.set('exhibition_id', exhibition_id);
|
data.set('exhibition_id', exhibition_id);
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
FROM registry.spiritofweb.ru/alpine:3.14
|
|
||||||
|
|
||||||
RUN apk --no-cache add openssh-client bash autossh sshpass
|
|
||||||
|
|
||||||
CMD []
|
|
||||||
Loading…
Reference in New Issue
Block a user