mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Merge branch 'main' of https://github.com/IRBorisov/ConceptPortal
# Conflicts: # rsconcept/frontend/src/components/Footer.tsx # rsconcept/frontend/src/components/Help/HelpLibrary.tsx # rsconcept/frontend/src/pages/RSFormPage/EditorRSForm.tsx
This commit is contained in:
commit
8a596ccccf
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
|
@ -8,28 +8,28 @@
|
||||||
"name": "Run",
|
"name": "Run",
|
||||||
"type": "PowerShell",
|
"type": "PowerShell",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunServer.ps1",
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Lint",
|
"name": "Lint",
|
||||||
"type": "PowerShell",
|
"type": "PowerShell",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunLint.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunLint.ps1",
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Test",
|
"name": "Test",
|
||||||
"type": "PowerShell",
|
"type": "PowerShell",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunTests.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunTests.ps1",
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BE-Coverage",
|
"name": "BE-Coverage",
|
||||||
"type": "PowerShell",
|
"type": "PowerShell",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunCoverage.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunCoverage.ps1",
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"name": "Restart",
|
"name": "Restart",
|
||||||
"type": "PowerShell",
|
"type": "PowerShell",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunServer.ps1",
|
||||||
"args": ["-freshStart"]
|
"args": ["-freshStart"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
46
README.md
46
README.md
|
@ -2,20 +2,11 @@
|
||||||
React + Django based web portal for editing RSForm schemas.
|
React + Django based web portal for editing RSForm schemas.
|
||||||
This readme file is used mostly to document project dependencies
|
This readme file is used mostly to document project dependencies
|
||||||
|
|
||||||
|
|
||||||
# Developer Setup Notes
|
|
||||||
- Install Python 3.9, NodeJS, VSCode, Docker Desktop
|
|
||||||
- copy import wheels from ConceptCore to rsconcept\backend\import
|
|
||||||
- run rsconcept\backend\LocalEnvSetup.ps1
|
|
||||||
- run 'npm install' in rsconcept\frontend
|
|
||||||
- use VSCode configs in root folder to start developement
|
|
||||||
- production: create secrets secrets\db_password.txt and django_key.txt
|
|
||||||
- production: provide TLS certificate nginx\cert\portal-cert.pem and nginx\cert\portal-key.pem
|
|
||||||
|
|
||||||
# Contributing notes
|
# Contributing notes
|
||||||
|
!BEFORE PUSHING INTO MAIN!
|
||||||
- use Test config in VSCode to run tests before pushing commits / requests
|
- use Test config in VSCode to run tests before pushing commits / requests
|
||||||
- !BEFORE PUSHING INTO MAIN! in rsconcept\frontend run in terminal 'npm run build' and fix all errors
|
- cd rsconcept/frontend & npm run build
|
||||||
- when making major changes make sure that Docker production is building correctly. run 'docker compose -f docker-compose-prod.yml up'
|
- docker compose -f docker-compose-prod.yml up
|
||||||
|
|
||||||
# Frontend stack & Tooling [Vite + React + Typescript]
|
# Frontend stack & Tooling [Vite + React + Typescript]
|
||||||
<details>
|
<details>
|
||||||
|
@ -62,11 +53,11 @@ This readme file is used mostly to document project dependencies
|
||||||
<details>
|
<details>
|
||||||
<summary>requirements</summary>
|
<summary>requirements</summary>
|
||||||
<pre>
|
<pre>
|
||||||
- tzdata
|
|
||||||
- django
|
- django
|
||||||
- djangorestframework
|
- djangorestframework
|
||||||
- django-cors-headers
|
- django-cors-headers
|
||||||
- django-filter
|
- django-filter
|
||||||
|
- tzdata
|
||||||
- gunicorn
|
- gunicorn
|
||||||
- coreapi
|
- coreapi
|
||||||
- psycopg2-binary
|
- psycopg2-binary
|
||||||
|
@ -96,3 +87,32 @@ This readme file is used mostly to document project dependencies
|
||||||
# DevOps
|
# DevOps
|
||||||
- Docker compose
|
- Docker compose
|
||||||
- PowerShell
|
- PowerShell
|
||||||
|
- Certbot
|
||||||
|
- Docker VSCode extension
|
||||||
|
|
||||||
|
# Developer Notes
|
||||||
|
## Local build (Windows 10+)
|
||||||
|
- this is main developers build
|
||||||
|
- Install Python 3.9, NodeJS, VSCode, Docker Desktop
|
||||||
|
- copy import wheels from ConceptCore to rsconcept/backend/import
|
||||||
|
- run rsconcept/backend/LocalEnvSetup.ps1
|
||||||
|
- run 'npm install' in rsconcept/frontend
|
||||||
|
- use VSCode configs in root folder to start developement
|
||||||
|
|
||||||
|
## Developement build
|
||||||
|
- this build does not use HTTPS and nginx for networking
|
||||||
|
- backend and frontend debugging is supported
|
||||||
|
- hmr (hot updates) for frontend
|
||||||
|
- run via 'docker compose -f "docker-compose-dev.yml" up --build -d'
|
||||||
|
- populate initial data: rsconcept/PopulateDevData.ps1 dev-portal-backend
|
||||||
|
|
||||||
|
## Local production build
|
||||||
|
- this build is same as production except not using production secrets and working on localhost
|
||||||
|
- provide TLS certificate (can be self-signed) 'nginx/cert/local-cert.pem' and 'nginx/cert/local-key.pem'
|
||||||
|
- run via 'docker compose -f "docker-compose-prod-local.yml" up --build -d'
|
||||||
|
- populate initial data: rsconcept/PopulateDevData.ps1 local-portal-backend
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
- create secrets secrets/db_password.txt and django_key.txt
|
||||||
|
- provide TLS certificate 'nginx/cert/front-cert.pem' and 'nginx/cert/front-key.pem'
|
||||||
|
- run via 'docker compose -f "docker-compose-prod.yml" up --build -d'
|
||||||
|
|
55
docker-compose-dev.yml
Normal file
55
docker-compose-dev.yml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: dev-concept-portal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_volume:
|
||||||
|
name: "dev-portal-data"
|
||||||
|
django_static_volume:
|
||||||
|
name: "dev-portal-static"
|
||||||
|
django_media_volume:
|
||||||
|
name: "dev-portal-media"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: dev-concept-api-net
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
container_name: dev-portal-frontend
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
build:
|
||||||
|
context: ./rsconcept/frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
BUILD_TYPE: development
|
||||||
|
ports:
|
||||||
|
- 3002:3002
|
||||||
|
command: npm run dev -- --host
|
||||||
|
|
||||||
|
|
||||||
|
backend:
|
||||||
|
container_name: dev-portal-backend
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgresql-db
|
||||||
|
build:
|
||||||
|
context: ./rsconcept/backend
|
||||||
|
env_file: ./rsconcept/backend/.env.dev
|
||||||
|
ports:
|
||||||
|
- 8002:8002
|
||||||
|
volumes:
|
||||||
|
- django_static_volume:/home/app/web/static
|
||||||
|
- django_media_volume:/home/app/web/media
|
||||||
|
command:
|
||||||
|
gunicorn -w 3 project.wsgi --bind 0.0.0.0:8002
|
||||||
|
|
||||||
|
|
||||||
|
postgresql-db:
|
||||||
|
container_name: dev-portal-db
|
||||||
|
restart: always
|
||||||
|
image: postgres:alpine
|
||||||
|
env_file: ./postgresql/.env.dev
|
||||||
|
volumes:
|
||||||
|
- postgres_volume:/var/lib/postgresql/data
|
||||||
|
|
71
docker-compose-prod-local.yml
Normal file
71
docker-compose-prod-local.yml
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
name: local-concept-portal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_volume:
|
||||||
|
name: "local-portal-data"
|
||||||
|
django_static_volume:
|
||||||
|
name: "local-portal-static"
|
||||||
|
django_media_volume:
|
||||||
|
name: "local-portal-media"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: local-concept-api-net
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
container_name: local-portal-frontend
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
build:
|
||||||
|
context: ./rsconcept/frontend
|
||||||
|
args:
|
||||||
|
BUILD_TYPE: production.local
|
||||||
|
expose:
|
||||||
|
- 3001
|
||||||
|
command: serve -s /home/node -l 3001
|
||||||
|
|
||||||
|
|
||||||
|
backend:
|
||||||
|
container_name: local-portal-backend
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgresql-db
|
||||||
|
build:
|
||||||
|
context: ./rsconcept/backend
|
||||||
|
env_file: ./rsconcept/backend/.env.prod.local
|
||||||
|
expose:
|
||||||
|
- 8001
|
||||||
|
volumes:
|
||||||
|
- django_static_volume:/home/app/web/static
|
||||||
|
- django_media_volume:/home/app/web/media
|
||||||
|
command:
|
||||||
|
gunicorn -w 3 project.wsgi --bind 0.0.0.0:8001
|
||||||
|
|
||||||
|
|
||||||
|
postgresql-db:
|
||||||
|
container_name: local-portal-db
|
||||||
|
restart: always
|
||||||
|
image: postgres:alpine
|
||||||
|
env_file: ./postgresql/.env.prod.local
|
||||||
|
volumes:
|
||||||
|
- postgres_volume:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
container_name: local-portal-router
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
args:
|
||||||
|
BUILD_TYPE: production.local
|
||||||
|
ports:
|
||||||
|
- 8001:8001
|
||||||
|
- 3001:3001
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||||
|
volumes:
|
||||||
|
- django_static_volume:/var/www/static
|
||||||
|
- django_media_volume:/var/www/media
|
|
@ -26,6 +26,8 @@ services:
|
||||||
- backend
|
- backend
|
||||||
build:
|
build:
|
||||||
context: ./rsconcept/frontend
|
context: ./rsconcept/frontend
|
||||||
|
args:
|
||||||
|
BUILD_TYPE: production
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
command: serve -s /home/node -l 3000
|
command: serve -s /home/node -l 3000
|
||||||
|
@ -72,6 +74,8 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
|
args:
|
||||||
|
BUILD_TYPE: production
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
FROM nginx:stable-alpine3.17-slim
|
FROM nginx:stable-alpine3.17-slim
|
||||||
|
ARG BUILD_TYPE=production
|
||||||
|
|
||||||
# Сopу nginx configuration to the proxy-server
|
# Сopу nginx configuration to the proxy-server
|
||||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
COPY ./$BUILD_TYPE.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY ./cert/* /etc/ssl/private/
|
COPY ./cert/* /etc/ssl/private/
|
|
@ -10,7 +10,7 @@ server {
|
||||||
listen 8000 ssl;
|
listen 8000 ssl;
|
||||||
ssl_certificate /etc/ssl/private/front-cert.pem;
|
ssl_certificate /etc/ssl/private/front-cert.pem;
|
||||||
ssl_certificate_key /etc/ssl/private/front-key.pem;
|
ssl_certificate_key /etc/ssl/private/front-key.pem;
|
||||||
server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru api.portal.acconcept.ru www.api.portal.acconcept.ru mail.acconcept.ru www.mail.acconcept.ru;
|
server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru api.portal.acconcept.ru www.api.portal.acconcept.ru;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@ -30,7 +30,7 @@ server {
|
||||||
listen 3000 ssl;
|
listen 3000 ssl;
|
||||||
ssl_certificate /etc/ssl/private/front-cert.pem;
|
ssl_certificate /etc/ssl/private/front-cert.pem;
|
||||||
ssl_certificate_key /etc/ssl/private/front-key.pem;
|
ssl_certificate_key /etc/ssl/private/front-key.pem;
|
||||||
server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru mail.acconcept.ru www.mail.acconcept.ru;
|
server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
41
nginx/production.local.conf
Normal file
41
nginx/production.local.conf
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
upstream innerdjango {
|
||||||
|
server backend:8001;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream innerreact {
|
||||||
|
server frontend:3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8001 ssl;
|
||||||
|
ssl_certificate /etc/ssl/private/local-cert.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/local-key.pem;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_pass http://innerdjango;
|
||||||
|
proxy_redirect default;
|
||||||
|
}
|
||||||
|
location /static/ {
|
||||||
|
alias /var/www/static/;
|
||||||
|
}
|
||||||
|
location /media/ {
|
||||||
|
alias /var/www/media/;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3001 ssl;
|
||||||
|
ssl_certificate /etc/ssl/private/local-cert.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/local-key.pem;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_pass http://innerreact;
|
||||||
|
proxy_redirect default;
|
||||||
|
}
|
||||||
|
}
|
5
postgresql/.env.dev
Normal file
5
postgresql/.env.dev
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# WARNING! This config does not use 'real' production values for secrets
|
||||||
|
# DO NOT use PRODUCTION LOCAL build for deployment!
|
||||||
|
POSTGRES_USER=portal-admin
|
||||||
|
POSTGRES_DB=portal-db
|
||||||
|
POSTGRES_PASSWORD=78ACF6C4F3
|
5
postgresql/.env.prod.local
Normal file
5
postgresql/.env.prod.local
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# WARNING! This config does not use 'real' production values for secrets
|
||||||
|
# DO NOT use PRODUCTION LOCAL build for deployment!
|
||||||
|
POSTGRES_USER=portal-admin
|
||||||
|
POSTGRES_DB=portal-db
|
||||||
|
POSTGRES_PASSWORD=78ACF6C4F3
|
|
@ -1,12 +0,0 @@
|
||||||
# Run coverage analysis
|
|
||||||
Set-Location $PSScriptRoot\backend
|
|
||||||
|
|
||||||
$coverageExec = "$PSScriptRoot\backend\venv\Scripts\coverage.exe"
|
|
||||||
$djangoSrc = "$PSScriptRoot\backend\manage.py"
|
|
||||||
$exclude = '*/venv/*,*/tests/*,*/migrations/*,*__init__.py,manage.py,apps.py,urls.py,settings.py'
|
|
||||||
|
|
||||||
& $coverageExec run --omit=$exclude $djangoSrc test
|
|
||||||
& $coverageExec report
|
|
||||||
& $coverageExec html
|
|
||||||
|
|
||||||
Start-Process "file:///$PSScriptRoot\backend\htmlcov\index.html"
|
|
|
@ -1,8 +0,0 @@
|
||||||
# Run coverage analysis
|
|
||||||
Set-Location $PSScriptRoot\backend
|
|
||||||
|
|
||||||
$pylint = "$PSScriptRoot\backend\venv\Scripts\pylint.exe"
|
|
||||||
$mypy = "$PSScriptRoot\backend\venv\Scripts\mypy.exe"
|
|
||||||
|
|
||||||
& $pylint cctext project apps
|
|
||||||
& $mypy cctext project apps
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Run tests
|
|
||||||
Set-Location $PSScriptRoot\backend
|
|
||||||
|
|
||||||
$pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe"
|
|
||||||
$djangoSrc = "$PSScriptRoot\backend\manage.py"
|
|
||||||
|
|
||||||
& $pyExec $djangoSrc check
|
|
||||||
& $pyExec $djangoSrc test
|
|
||||||
|
|
||||||
Set-Location $PSScriptRoot\frontend
|
|
||||||
|
|
||||||
& npm test
|
|
27
rsconcept/backend/.env.dev
Normal file
27
rsconcept/backend/.env.dev
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Application settings
|
||||||
|
# WARNING! This config does not use 'real' production values for secrets
|
||||||
|
# DO NOT use PRODUCTION LOCAL build for deployment!
|
||||||
|
SECRET_KEY=s-55j!5jlan=x%8-6m1qnst^7s6nwby4dx@vei)5w8t)3_=mv1
|
||||||
|
ALLOWED_HOSTS=localhost
|
||||||
|
CSRF_TRUSTED_ORIGINS=http://localhost:3002;http://localhost:8002
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3002
|
||||||
|
|
||||||
|
|
||||||
|
# File locations
|
||||||
|
STATIC_ROOT=/home/app/web/static
|
||||||
|
MEDIA_ROOT=/home/app/web/media
|
||||||
|
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_NAME=portal-db
|
||||||
|
DB_USER=portal-admin
|
||||||
|
DB_HOST=postgresql-db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_PASSWORD=78ACF6C4F3
|
||||||
|
|
||||||
|
|
||||||
|
# Debug settings
|
||||||
|
DEBUG=1
|
||||||
|
PYTHONDEVMODE=1
|
||||||
|
PYTHONTRACEMALLOC=1
|
|
@ -1,5 +1,6 @@
|
||||||
# Application settings
|
# Application settings
|
||||||
|
|
||||||
|
# SECRET_KEY=
|
||||||
ALLOWED_HOSTS=portal.acconcept.ru;dev.concept.ru
|
ALLOWED_HOSTS=portal.acconcept.ru;dev.concept.ru
|
||||||
CSRF_TRUSTED_ORIGINS=https://dev.concept.ru:3000;https://dev.concept.ru:8000;https://portal.acconcept.ru;https://portal.acconcept.ru:8081;https://portal.acconcept.ru:8082
|
CSRF_TRUSTED_ORIGINS=https://dev.concept.ru:3000;https://dev.concept.ru:8000;https://portal.acconcept.ru;https://portal.acconcept.ru:8081;https://portal.acconcept.ru:8082
|
||||||
CORS_ALLOWED_ORIGINS=https://dev.concept.ru:3000;https://portal.acconcept.ru;https://portal.acconcept.ru:8081
|
CORS_ALLOWED_ORIGINS=https://dev.concept.ru:3000;https://portal.acconcept.ru;https://portal.acconcept.ru:8081
|
||||||
|
@ -16,6 +17,7 @@ DB_NAME=portal-db
|
||||||
DB_USER=portal-admin
|
DB_USER=portal-admin
|
||||||
DB_HOST=postgresql-db
|
DB_HOST=postgresql-db
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
|
||||||
# Debug settings
|
# Debug settings
|
||||||
|
|
27
rsconcept/backend/.env.prod.local
Normal file
27
rsconcept/backend/.env.prod.local
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Application settings
|
||||||
|
# WARNING! This config does not use 'real' production values for secrets
|
||||||
|
# DO NOT use PRODUCTION LOCAL build for deployment!
|
||||||
|
SECRET_KEY=s-55j!5jlan=x%8-6m1qnst^7s6nwby4dx@vei)5w8t)3_=mv1
|
||||||
|
ALLOWED_HOSTS=localhost
|
||||||
|
CSRF_TRUSTED_ORIGINS=https://localhost:3001;https://localhost:8001
|
||||||
|
CORS_ALLOWED_ORIGINS=https://localhost:3001
|
||||||
|
|
||||||
|
|
||||||
|
# File locations
|
||||||
|
STATIC_ROOT=/home/app/web/static
|
||||||
|
MEDIA_ROOT=/home/app/web/media
|
||||||
|
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_NAME=portal-db
|
||||||
|
DB_USER=portal-admin
|
||||||
|
DB_HOST=postgresql-db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_PASSWORD=78ACF6C4F3
|
||||||
|
|
||||||
|
|
||||||
|
# Debug settings
|
||||||
|
DEBUG=0
|
||||||
|
PYTHONDEVMODE=0
|
||||||
|
PYTHONTRACEMALLOC=0
|
|
@ -46,6 +46,7 @@ RUN mkdir -p $USER_HOME && \
|
||||||
mkdir -p $APP_HOME && \
|
mkdir -p $APP_HOME && \
|
||||||
mkdir -p $APP_HOME/static && \
|
mkdir -p $APP_HOME/static && \
|
||||||
mkdir -p $APP_HOME/media && \
|
mkdir -p $APP_HOME/media && \
|
||||||
|
mkdir -p $APP_HOME/backup && \
|
||||||
adduser --system --group app
|
adduser --system --group app
|
||||||
|
|
||||||
# Install python dependencies
|
# Install python dependencies
|
||||||
|
@ -64,7 +65,8 @@ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
|
||||||
chmod +x $APP_HOME/entrypoint.sh && \
|
chmod +x $APP_HOME/entrypoint.sh && \
|
||||||
chown -R app:app $APP_HOME && \
|
chown -R app:app $APP_HOME && \
|
||||||
chmod -R a+rwx $APP_HOME/static && \
|
chmod -R a+rwx $APP_HOME/static && \
|
||||||
chmod -R a+rwx $APP_HOME/media
|
chmod -R a+rwx $APP_HOME/media && \
|
||||||
|
chmod -R a+rwx $APP_HOME/backup
|
||||||
|
|
||||||
RUN
|
RUN
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Script creates venv and installs dependencies + imports
|
|
||||||
Set-Location $PSScriptRoot
|
|
||||||
|
|
||||||
$envPath = "$PSScriptRoot\venv"
|
|
||||||
$python = "$envPath\Scripts\python.exe"
|
|
||||||
|
|
||||||
if (Test-Path -Path $envPath) {
|
|
||||||
Write-Host "Removing previous env: $envPath`n" -ForegroundColor DarkGreen
|
|
||||||
Remove-Item $envPath -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Creating python env: $envPath`n" -ForegroundColor DarkGreen
|
|
||||||
& 'python' -m venv $envPath
|
|
||||||
& $python -m pip install --upgrade pip
|
|
||||||
& $python -m pip install -r requirements_dev.txt
|
|
||||||
|
|
||||||
$wheel = Get-Childitem -Path import\*win*.whl -Name
|
|
||||||
if (-not $wheel) {
|
|
||||||
Write-Error 'Missing import wheel'
|
|
||||||
Exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Installing wheel: $wheel`n" -ForegroundColor DarkGreen
|
|
||||||
& $python -m pip install -I import\$wheel
|
|
|
@ -1,6 +1,4 @@
|
||||||
''' Models: RSForms for conceptual schemas. '''
|
''' Models: RSForms for conceptual schemas. '''
|
||||||
import json
|
|
||||||
from copy import deepcopy
|
|
||||||
import re
|
import re
|
||||||
from typing import Iterable, Optional, cast
|
from typing import Iterable, Optional, cast
|
||||||
|
|
||||||
|
@ -13,7 +11,6 @@ from django.core.validators import MinValueValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import pyconcept
|
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from cctext import Resolver, Entity, extract_entities
|
from cctext import Resolver, Entity, extract_entities
|
||||||
from .graph import Graph
|
from .graph import Graph
|
||||||
|
@ -315,6 +312,8 @@ class RSForm:
|
||||||
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
|
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
|
||||||
if position <= 0:
|
if position <= 0:
|
||||||
raise ValidationError('Invalid position: should be positive integer')
|
raise ValidationError('Invalid position: should be positive integer')
|
||||||
|
currentSize = self.constituents().count()
|
||||||
|
position = max(1, min(position, currentSize + 1))
|
||||||
update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self.item, order__gte=position)
|
update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self.item, order__gte=position)
|
||||||
for cst in update_list:
|
for cst in update_list:
|
||||||
cst.order += 1
|
cst.order += 1
|
||||||
|
@ -326,7 +325,6 @@ class RSForm:
|
||||||
alias=alias,
|
alias=alias,
|
||||||
cst_type=insert_type
|
cst_type=insert_type
|
||||||
)
|
)
|
||||||
self.update_order()
|
|
||||||
self.item.save()
|
self.item.save()
|
||||||
result.refresh_from_db()
|
result.refresh_from_db()
|
||||||
return result
|
return result
|
||||||
|
@ -343,7 +341,6 @@ class RSForm:
|
||||||
alias=alias,
|
alias=alias,
|
||||||
cst_type=insert_type
|
cst_type=insert_type
|
||||||
)
|
)
|
||||||
self.update_order()
|
|
||||||
self.item.save()
|
self.item.save()
|
||||||
result.refresh_from_db()
|
result.refresh_from_db()
|
||||||
return result
|
return result
|
||||||
|
@ -369,7 +366,6 @@ class RSForm:
|
||||||
count_moved += 1
|
count_moved += 1
|
||||||
update_list.append(cst)
|
update_list.append(cst)
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||||
self.update_order()
|
|
||||||
self.item.save()
|
self.item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -377,7 +373,7 @@ class RSForm:
|
||||||
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
||||||
for cst in listCst:
|
for cst in listCst:
|
||||||
cst.delete()
|
cst.delete()
|
||||||
self.update_order()
|
self._reset_order()
|
||||||
self.resolve_all_text()
|
self.resolve_all_text()
|
||||||
self.item.save()
|
self.item.save()
|
||||||
|
|
||||||
|
@ -447,23 +443,6 @@ class RSForm:
|
||||||
if modified:
|
if modified:
|
||||||
cst.save()
|
cst.save()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update_order(self):
|
|
||||||
''' Update constituents order. '''
|
|
||||||
checked = PyConceptAdapter(self).basic()
|
|
||||||
update_list = self.constituents().only('id', 'order')
|
|
||||||
if len(checked['items']) != update_list.count():
|
|
||||||
raise ValidationError('Invalid constituents count')
|
|
||||||
order = 1
|
|
||||||
for cst in checked['items']:
|
|
||||||
cst_id = cst['id']
|
|
||||||
for oldCst in update_list:
|
|
||||||
if oldCst.pk == cst_id:
|
|
||||||
oldCst.order = order
|
|
||||||
order += 1
|
|
||||||
break
|
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def resolve_all_text(self):
|
def resolve_all_text(self):
|
||||||
''' Trigger reference resolution for all texts. '''
|
''' Trigger reference resolution for all texts. '''
|
||||||
|
@ -482,6 +461,15 @@ class RSForm:
|
||||||
cst.definition_resolved = resolved
|
cst.definition_resolved = resolved
|
||||||
cst.save()
|
cst.save()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def _reset_order(self):
|
||||||
|
order = 1
|
||||||
|
for cst in self.constituents().only('id', 'order').order_by('order'):
|
||||||
|
if cst.order != order:
|
||||||
|
cst.order = order
|
||||||
|
cst.save()
|
||||||
|
order += 1
|
||||||
|
|
||||||
def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
|
def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
|
||||||
if insert_after is not None:
|
if insert_after is not None:
|
||||||
cstafter = Constituenta.objects.get(pk=insert_after)
|
cstafter = Constituenta.objects.get(pk=insert_after)
|
||||||
|
@ -510,87 +498,3 @@ class RSForm:
|
||||||
if result.contains(alias):
|
if result.contains(alias):
|
||||||
result.add_edge(id_from=alias, id_to=cst.alias)
|
result.add_edge(id_from=alias, id_to=cst.alias)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class PyConceptAdapter:
|
|
||||||
''' RSForm adapter for interacting with pyconcept module. '''
|
|
||||||
def __init__(self, instance: RSForm):
|
|
||||||
self.schema = instance
|
|
||||||
self.data = self._prepare_request()
|
|
||||||
self._checked_data: Optional[dict] = None
|
|
||||||
|
|
||||||
def basic(self) -> dict:
|
|
||||||
''' Check RSForm and return check results.
|
|
||||||
Warning! Does not include texts. '''
|
|
||||||
self._produce_response()
|
|
||||||
if self._checked_data is None:
|
|
||||||
raise ValueError('Invalid data response from pyconcept')
|
|
||||||
return self._checked_data
|
|
||||||
|
|
||||||
def full(self) -> dict:
|
|
||||||
''' Check RSForm and return check results including initial texts. '''
|
|
||||||
self._produce_response()
|
|
||||||
if self._checked_data is None:
|
|
||||||
raise ValueError('Invalid data response from pyconcept')
|
|
||||||
return self._complete_rsform_details(self._checked_data)
|
|
||||||
|
|
||||||
def _complete_rsform_details(self, data: dict) -> dict:
|
|
||||||
result = deepcopy(data)
|
|
||||||
result['id'] = self.schema.item.pk
|
|
||||||
result['alias'] = self.schema.item.alias
|
|
||||||
result['title'] = self.schema.item.title
|
|
||||||
result['comment'] = self.schema.item.comment
|
|
||||||
result['time_update'] = self.schema.item.time_update
|
|
||||||
result['time_create'] = self.schema.item.time_create
|
|
||||||
result['is_common'] = self.schema.item.is_common
|
|
||||||
result['is_canonical'] = self.schema.item.is_canonical
|
|
||||||
result['owner'] = (self.schema.item.owner.pk if self.schema.item.owner is not None else None)
|
|
||||||
for cst_data in result['items']:
|
|
||||||
cst = Constituenta.objects.get(pk=cst_data['id'])
|
|
||||||
cst_data['convention'] = cst.convention
|
|
||||||
cst_data['term'] = {
|
|
||||||
'raw': cst.term_raw,
|
|
||||||
'resolved': cst.term_resolved,
|
|
||||||
'forms': cst.term_forms
|
|
||||||
}
|
|
||||||
cst_data['definition']['text'] = {
|
|
||||||
'raw': cst.definition_raw,
|
|
||||||
'resolved': cst.definition_resolved,
|
|
||||||
}
|
|
||||||
result['subscribers'] = [item.pk for item in self.schema.item.subscribers()]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _prepare_request(self) -> dict:
|
|
||||||
result: dict = {
|
|
||||||
'items': []
|
|
||||||
}
|
|
||||||
items = self.schema.constituents().order_by('order')
|
|
||||||
for cst in items:
|
|
||||||
result['items'].append({
|
|
||||||
'entityUID': cst.pk,
|
|
||||||
'cstType': cst.cst_type,
|
|
||||||
'alias': cst.alias,
|
|
||||||
'definition': {
|
|
||||||
'formal': cst.definition_formal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _produce_response(self):
|
|
||||||
if self._checked_data is not None:
|
|
||||||
return
|
|
||||||
response = pyconcept.check_schema(json.dumps(self.data))
|
|
||||||
data = json.loads(response)
|
|
||||||
self._checked_data = {
|
|
||||||
'items': []
|
|
||||||
}
|
|
||||||
for cst in data['items']:
|
|
||||||
self._checked_data['items'].append({
|
|
||||||
'id': cst['entityUID'],
|
|
||||||
'cstType': cst['cstType'],
|
|
||||||
'alias': cst['alias'],
|
|
||||||
'definition': {
|
|
||||||
'formal': cst['definition']['formal']
|
|
||||||
},
|
|
||||||
'parse': cst['parse']
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
''' Serializers for conceptual schema API. '''
|
''' Serializers for conceptual schema API. '''
|
||||||
|
import json
|
||||||
from typing import Optional, cast
|
from typing import Optional, cast
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
import pyconcept
|
||||||
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
|
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
|
||||||
|
|
||||||
from .utils import fix_old_references
|
from .utils import fix_old_references
|
||||||
|
@ -31,7 +33,7 @@ class TextSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class LibraryItemSerializer(serializers.ModelSerializer):
|
class LibraryItemSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Library item data. '''
|
''' Serializer: LibraryItem entry. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = LibraryItem
|
model = LibraryItem
|
||||||
|
@ -39,6 +41,71 @@ class LibraryItemSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('owner', 'id', 'item_type')
|
read_only_fields = ('owner', 'id', 'item_type')
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
||||||
|
''' Serializer: LibraryItem detailed data. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = LibraryItem
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ('owner', 'id', 'item_type')
|
||||||
|
|
||||||
|
def to_representation(self, instance: LibraryItem):
|
||||||
|
result = super().to_representation(instance)
|
||||||
|
result['subscribers'] = [item.pk for item in instance.subscribers()]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PyConceptAdapter:
|
||||||
|
''' RSForm adapter for interacting with pyconcept module. '''
|
||||||
|
def __init__(self, instance: RSForm):
|
||||||
|
self.schema = instance
|
||||||
|
self.data = self._prepare_request()
|
||||||
|
self._checked_data: Optional[dict] = None
|
||||||
|
|
||||||
|
def parse(self) -> dict:
|
||||||
|
''' Check RSForm and return check results.
|
||||||
|
Warning! Does not include texts. '''
|
||||||
|
self._produce_response()
|
||||||
|
if self._checked_data is None:
|
||||||
|
raise ValueError('Invalid data response from pyconcept')
|
||||||
|
return self._checked_data
|
||||||
|
|
||||||
|
def _prepare_request(self) -> dict:
|
||||||
|
result: dict = {
|
||||||
|
'items': []
|
||||||
|
}
|
||||||
|
items = self.schema.constituents().order_by('order')
|
||||||
|
for cst in items:
|
||||||
|
result['items'].append({
|
||||||
|
'entityUID': cst.pk,
|
||||||
|
'cstType': cst.cst_type,
|
||||||
|
'alias': cst.alias,
|
||||||
|
'definition': {
|
||||||
|
'formal': cst.definition_formal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _produce_response(self):
|
||||||
|
if self._checked_data is not None:
|
||||||
|
return
|
||||||
|
response = pyconcept.check_schema(json.dumps(self.data))
|
||||||
|
data = json.loads(response)
|
||||||
|
self._checked_data = {
|
||||||
|
'items': []
|
||||||
|
}
|
||||||
|
for cst in data['items']:
|
||||||
|
self._checked_data['items'].append({
|
||||||
|
'id': cst['entityUID'],
|
||||||
|
'cstType': cst['cstType'],
|
||||||
|
'alias': cst['alias'],
|
||||||
|
'definition': {
|
||||||
|
'formal': cst['definition']['formal']
|
||||||
|
},
|
||||||
|
'parse': cst['parse']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class RSFormSerializer(serializers.ModelSerializer):
|
class RSFormSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Detailed data for RSForm. '''
|
''' Serializer: Detailed data for RSForm. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -46,13 +113,30 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
model = RSForm
|
model = RSForm
|
||||||
|
|
||||||
def to_representation(self, instance: RSForm):
|
def to_representation(self, instance: RSForm):
|
||||||
result = LibraryItemSerializer(instance.item).data
|
result = LibraryItemDetailsSerializer(instance.item).data
|
||||||
result['items'] = []
|
result['items'] = []
|
||||||
for cst in instance.constituents().order_by('order'):
|
for cst in instance.constituents().order_by('order'):
|
||||||
result['items'].append(ConstituentaSerializer(cst).data)
|
result['items'].append(ConstituentaSerializer(cst).data)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class RSFormParseSerializer(serializers.ModelSerializer):
|
||||||
|
''' Serializer: Detailed data for RSForm including parse. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = RSForm
|
||||||
|
|
||||||
|
def to_representation(self, instance: RSForm):
|
||||||
|
result = RSFormSerializer(instance).data
|
||||||
|
parse = PyConceptAdapter(instance).parse()
|
||||||
|
for cst_data in result['items']:
|
||||||
|
cst_data['parse'] = next(
|
||||||
|
cst['parse'] for cst in parse['items']
|
||||||
|
if cst['id'] == cst_data['id']
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class RSFormUploadSerializer(serializers.Serializer):
|
class RSFormUploadSerializer(serializers.Serializer):
|
||||||
''' Upload data for RSForm serializer. '''
|
''' Upload data for RSForm serializer. '''
|
||||||
file = serializers.FileField()
|
file = serializers.FileField()
|
||||||
|
@ -193,7 +277,6 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
if prev_cst.pk not in loaded_ids:
|
if prev_cst.pk not in loaded_ids:
|
||||||
prev_cst.delete()
|
prev_cst.delete()
|
||||||
|
|
||||||
instance.update_order()
|
|
||||||
instance.resolve_all_text()
|
instance.resolve_all_text()
|
||||||
instance.item.save()
|
instance.item.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
|
@ -200,10 +200,10 @@ class TestRSForm(TestCase):
|
||||||
d2 = schema.insert_at(1, 'D2', CstType.TERM)
|
d2 = schema.insert_at(1, 'D2', CstType.TERM)
|
||||||
d1.refresh_from_db()
|
d1.refresh_from_db()
|
||||||
self.assertEqual(d1.order, 3)
|
self.assertEqual(d1.order, 3)
|
||||||
self.assertEqual(d2.order, 2)
|
self.assertEqual(d2.order, 1)
|
||||||
|
|
||||||
x2 = schema.insert_at(4, 'X2', CstType.BASE)
|
x2 = schema.insert_at(4, 'X2', CstType.BASE)
|
||||||
self.assertEqual(x2.order, 2)
|
self.assertEqual(x2.order, 4)
|
||||||
|
|
||||||
def test_insert_last(self):
|
def test_insert_last(self):
|
||||||
schema = RSForm.create(title='Test')
|
schema = RSForm.create(title='Test')
|
||||||
|
@ -259,10 +259,10 @@ class TestRSForm(TestCase):
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
d1.refresh_from_db()
|
d1.refresh_from_db()
|
||||||
d2.refresh_from_db()
|
d2.refresh_from_db()
|
||||||
self.assertEqual(x1.order, 2)
|
self.assertEqual(x1.order, 3)
|
||||||
self.assertEqual(x2.order, 1)
|
self.assertEqual(x2.order, 1)
|
||||||
self.assertEqual(d1.order, 4)
|
self.assertEqual(d1.order, 4)
|
||||||
self.assertEqual(d2.order, 3)
|
self.assertEqual(d2.order, 2)
|
||||||
|
|
||||||
def test_move_cst_down(self):
|
def test_move_cst_down(self):
|
||||||
schema = RSForm.create(title='Test')
|
schema = RSForm.create(title='Test')
|
||||||
|
|
|
@ -213,10 +213,14 @@ class TestLibraryViewset(APITestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertFalse(_response_contains(response, self.unowned))
|
self.assertFalse(_response_contains(response, self.unowned))
|
||||||
|
|
||||||
|
user2 = User.objects.create(username='UserTest2')
|
||||||
Subscription.subscribe(user=self.user, item=self.unowned)
|
Subscription.subscribe(user=self.user, item=self.unowned)
|
||||||
|
Subscription.subscribe(user=user2, item=self.unowned)
|
||||||
|
Subscription.subscribe(user=user2, item=self.owned)
|
||||||
response = self.client.get('/api/library/active')
|
response = self.client.get('/api/library/active')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(_response_contains(response, self.unowned))
|
self.assertTrue(_response_contains(response, self.unowned))
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
def test_subscriptions(self):
|
def test_subscriptions(self):
|
||||||
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
||||||
|
@ -287,11 +291,11 @@ class TestRSFormViewset(APITestCase):
|
||||||
self.assertEqual(len(response.data['items']), 2)
|
self.assertEqual(len(response.data['items']), 2)
|
||||||
self.assertEqual(response.data['items'][0]['id'], x1.id)
|
self.assertEqual(response.data['items'][0]['id'], x1.id)
|
||||||
self.assertEqual(response.data['items'][0]['parse']['status'], 'verified')
|
self.assertEqual(response.data['items'][0]['parse']['status'], 'verified')
|
||||||
self.assertEqual(response.data['items'][0]['term']['raw'], x1.term_raw)
|
self.assertEqual(response.data['items'][0]['term_raw'], x1.term_raw)
|
||||||
self.assertEqual(response.data['items'][0]['term']['resolved'], x1.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x1.term_resolved)
|
||||||
self.assertEqual(response.data['items'][1]['id'], x2.id)
|
self.assertEqual(response.data['items'][1]['id'], x2.id)
|
||||||
self.assertEqual(response.data['items'][1]['term']['raw'], x2.term_raw)
|
self.assertEqual(response.data['items'][1]['term_raw'], x2.term_raw)
|
||||||
self.assertEqual(response.data['items'][1]['term']['resolved'], x2.term_resolved)
|
self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved)
|
||||||
self.assertEqual(response.data['subscribers'], [self.user.pk])
|
self.assertEqual(response.data['subscribers'], [self.user.pk])
|
||||||
|
|
||||||
def test_check(self):
|
def test_check(self):
|
||||||
|
@ -412,6 +416,7 @@ class TestRSFormViewset(APITestCase):
|
||||||
d1.definition_formal = 'X1'
|
d1.definition_formal = 'X1'
|
||||||
d1.save()
|
d1.save()
|
||||||
|
|
||||||
|
self.assertEqual(d1.order, 4)
|
||||||
self.assertEqual(self.cst1.order, 1)
|
self.assertEqual(self.cst1.order, 1)
|
||||||
self.assertEqual(self.cst1.alias, 'X1')
|
self.assertEqual(self.cst1.alias, 'X1')
|
||||||
self.assertEqual(self.cst1.cst_type, CstType.BASE)
|
self.assertEqual(self.cst1.cst_type, CstType.BASE)
|
||||||
|
@ -422,9 +427,10 @@ class TestRSFormViewset(APITestCase):
|
||||||
self.assertEqual(response.data['new_cst']['cst_type'], 'term')
|
self.assertEqual(response.data['new_cst']['cst_type'], 'term')
|
||||||
d1.refresh_from_db()
|
d1.refresh_from_db()
|
||||||
self.cst1.refresh_from_db()
|
self.cst1.refresh_from_db()
|
||||||
|
self.assertEqual(d1.order, 4)
|
||||||
self.assertEqual(d1.term_resolved, '')
|
self.assertEqual(d1.term_resolved, '')
|
||||||
self.assertEqual(d1.term_raw, '@{D2|plur}')
|
self.assertEqual(d1.term_raw, '@{D2|plur}')
|
||||||
self.assertEqual(self.cst1.order, 2)
|
self.assertEqual(self.cst1.order, 1)
|
||||||
self.assertEqual(self.cst1.alias, 'D2')
|
self.assertEqual(self.cst1.alias, 'D2')
|
||||||
self.assertEqual(self.cst1.cst_type, CstType.TERM)
|
self.assertEqual(self.cst1.cst_type, CstType.TERM)
|
||||||
|
|
||||||
|
@ -560,10 +566,10 @@ class TestRSFormViewset(APITestCase):
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(response.data['title'], 'Title')
|
self.assertEqual(response.data['title'], 'Title')
|
||||||
self.assertEqual(response.data['items'][0]['alias'], x1.alias)
|
self.assertEqual(response.data['items'][0]['alias'], x1.alias)
|
||||||
self.assertEqual(response.data['items'][0]['term']['raw'], x1.term_raw)
|
self.assertEqual(response.data['items'][0]['term_raw'], x1.term_raw)
|
||||||
self.assertEqual(response.data['items'][0]['term']['resolved'], x1.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x1.term_resolved)
|
||||||
self.assertEqual(response.data['items'][1]['term']['raw'], d1.term_raw)
|
self.assertEqual(response.data['items'][1]['term_raw'], d1.term_raw)
|
||||||
self.assertEqual(response.data['items'][1]['term']['resolved'], d1.term_resolved)
|
self.assertEqual(response.data['items'][1]['term_resolved'], d1.term_resolved)
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionalViews(APITestCase):
|
class TestFunctionalViews(APITestCase):
|
||||||
|
|
|
@ -25,7 +25,9 @@ class LibraryActiveView(generics.ListAPIView):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if not user.is_anonymous:
|
if not user.is_anonymous:
|
||||||
# pylint: disable=unsupported-binary-operation
|
# pylint: disable=unsupported-binary-operation
|
||||||
return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user))
|
return m.LibraryItem.objects.filter(
|
||||||
|
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
|
||||||
|
).distinct()
|
||||||
else:
|
else:
|
||||||
return m.LibraryItem.objects.filter(is_common=True)
|
return m.LibraryItem.objects.filter(is_common=True)
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
||||||
clone.is_valid(raise_exception=True)
|
clone.is_valid(raise_exception=True)
|
||||||
new_schema = clone.save()
|
new_schema = clone.save()
|
||||||
return Response(status=201, data=m.PyConceptAdapter(new_schema).full())
|
return Response(status=201, data=s.RSFormParseSerializer(new_schema).data)
|
||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -153,7 +155,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
schema.item.refresh_from_db()
|
schema.item.refresh_from_db()
|
||||||
response = Response(status=201, data={
|
response = Response(status=201, data={
|
||||||
'new_cst': s.ConstituentaSerializer(new_cst).data,
|
'new_cst': s.ConstituentaSerializer(new_cst).data,
|
||||||
'schema': m.PyConceptAdapter(schema).full()
|
'schema': s.RSFormParseSerializer(schema).data
|
||||||
})
|
})
|
||||||
response['Location'] = new_cst.get_absolute_url()
|
response['Location'] = new_cst.get_absolute_url()
|
||||||
return response
|
return response
|
||||||
|
@ -169,12 +171,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer.save()
|
serializer.save()
|
||||||
mapping = { old_alias: serializer.validated_data['alias'] }
|
mapping = { old_alias: serializer.validated_data['alias'] }
|
||||||
schema.apply_mapping(mapping, change_aliases=False)
|
schema.apply_mapping(mapping, change_aliases=False)
|
||||||
schema.update_order()
|
|
||||||
schema.item.refresh_from_db()
|
schema.item.refresh_from_db()
|
||||||
cst = m.Constituenta.objects.get(pk=serializer.validated_data['id'])
|
cst = m.Constituenta.objects.get(pk=serializer.validated_data['id'])
|
||||||
return Response(status=200, data={
|
return Response(status=200, data={
|
||||||
'new_cst': s.ConstituentaSerializer(cst).data,
|
'new_cst': s.ConstituentaSerializer(cst).data,
|
||||||
'schema': m.PyConceptAdapter(schema).full()
|
'schema': s.RSFormParseSerializer(schema).data
|
||||||
})
|
})
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
||||||
|
@ -185,7 +186,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.delete_cst(serializer.validated_data['constituents'])
|
schema.delete_cst(serializer.validated_data['constituents'])
|
||||||
schema.item.refresh_from_db()
|
schema.item.refresh_from_db()
|
||||||
return Response(status=202, data=m.PyConceptAdapter(schema).full())
|
return Response(status=202, data=s.RSFormParseSerializer(schema).data)
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||||
def cst_moveto(self, request, pk):
|
def cst_moveto(self, request, pk):
|
||||||
|
@ -195,14 +196,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||||
schema.item.refresh_from_db()
|
schema.item.refresh_from_db()
|
||||||
return Response(status=200, data=m.PyConceptAdapter(schema).full())
|
return Response(status=200, data=s.RSFormParseSerializer(schema).data)
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||||
def reset_aliases(self, request, pk):
|
def reset_aliases(self, request, pk):
|
||||||
''' Endpoint: Recreate all aliases based on order. '''
|
''' Endpoint: Recreate all aliases based on order. '''
|
||||||
schema = self._get_schema()
|
schema = self._get_schema()
|
||||||
schema.reset_aliases()
|
schema.reset_aliases()
|
||||||
return Response(status=200, data=m.PyConceptAdapter(schema).full())
|
return Response(status=200, data=s.RSFormParseSerializer(schema).data)
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'], url_path='load-trs')
|
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||||
def load_trs(self, request, pk):
|
def load_trs(self, request, pk):
|
||||||
|
@ -217,7 +218,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
|
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema = serializer.save()
|
schema = serializer.save()
|
||||||
return Response(status=200, data=m.PyConceptAdapter(schema).full())
|
return Response(status=200, data=s.RSFormParseSerializer(schema).data)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def contents(self, request, pk):
|
def contents(self, request, pk):
|
||||||
|
@ -229,27 +230,26 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
def details(self, request, pk):
|
def details(self, request, pk):
|
||||||
''' Endpoint: Detailed schema view including statuses and parse. '''
|
''' Endpoint: Detailed schema view including statuses and parse. '''
|
||||||
schema = self._get_schema()
|
schema = self._get_schema()
|
||||||
serializer = m.PyConceptAdapter(schema)
|
serializer = s.RSFormParseSerializer(schema)
|
||||||
return Response(serializer.full())
|
return Response(serializer.data)
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def check(self, request, pk):
|
def check(self, request, pk):
|
||||||
''' Endpoint: Check RSLang expression against schema context. '''
|
''' Endpoint: Check RSLang expression against schema context. '''
|
||||||
schema = m.PyConceptAdapter(self._get_schema())
|
|
||||||
serializer = s.ExpressionSerializer(data=request.data)
|
serializer = s.ExpressionSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
expression = serializer.validated_data['expression']
|
expression = serializer.validated_data['expression']
|
||||||
|
schema = s.PyConceptAdapter(self._get_schema())
|
||||||
result = pyconcept.check_expression(json.dumps(schema.data), expression)
|
result = pyconcept.check_expression(json.dumps(schema.data), expression)
|
||||||
return Response(json.loads(result))
|
return Response(json.loads(result))
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def resolve(self, request, pk):
|
def resolve(self, request, pk):
|
||||||
''' Endpoint: Resolve refenrces in text against schema terms context. '''
|
''' Endpoint: Resolve refenrces in text against schema terms context. '''
|
||||||
schema = self._get_schema()
|
|
||||||
serializer = s.TextSerializer(data=request.data)
|
serializer = s.TextSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
text = serializer.validated_data['text']
|
text = serializer.validated_data['text']
|
||||||
resolver = schema.resolver()
|
resolver = self._get_schema().resolver()
|
||||||
resolver.resolve(text)
|
resolver.resolve(text)
|
||||||
return Response(status=200, data=s.ResolverSerializer(resolver).data)
|
return Response(status=200, data=s.ResolverSerializer(resolver).data)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# Dev specific
|
# Dev specific
|
||||||
.gitignore
|
.gitignore
|
||||||
node_modules
|
node_modules
|
||||||
|
.env.local
|
5
rsconcept/frontend/.env.local
Normal file
5
rsconcept/frontend/.env.local
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Local build config
|
||||||
|
|
||||||
|
VITE_PORTAL_BACKEND=http://localhost:8000
|
||||||
|
VITE_PORTAL_FRONT_PORT=3000
|
||||||
|
VITE_PORTAL_FRONT_HTTPS=false
|
|
@ -5,11 +5,14 @@ RUN apt-get update -qq && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ======= Build =======
|
# ======= Build =======
|
||||||
|
ARG BUILD_TYPE=production
|
||||||
FROM node-base as builder
|
FROM node-base as builder
|
||||||
|
|
||||||
WORKDIR /result
|
WORKDIR /result
|
||||||
|
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
|
COPY ./env/.env.$BUILD_TYPE ./
|
||||||
|
RUN rm -rf ./env
|
||||||
RUN npm install
|
RUN npm install
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
17
rsconcept/frontend/Dockerfile.dev
Normal file
17
rsconcept/frontend/Dockerfile.dev
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# ======== Multi-stage base ==========
|
||||||
|
FROM node:bullseye-slim as node-base
|
||||||
|
RUN apt-get update -qq && \
|
||||||
|
apt-get upgrade -y && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ========= Server =======
|
||||||
|
FROM node-base as product-server
|
||||||
|
ARG BUILD_TYPE=production
|
||||||
|
|
||||||
|
WORKDIR /home
|
||||||
|
|
||||||
|
COPY ./ ./
|
||||||
|
COPY ./env/.env.$BUILD_TYPE ./
|
||||||
|
RUN rm -rf ./env
|
||||||
|
|
||||||
|
RUN npm install
|
5
rsconcept/frontend/env/.env.development
vendored
Normal file
5
rsconcept/frontend/env/.env.development
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Frontend public settings: Production Local
|
||||||
|
|
||||||
|
VITE_PORTAL_BACKEND=http://localhost:8002
|
||||||
|
VITE_PORTAL_FRONT_PORT=3002
|
||||||
|
VITE_PORTAL_FRONT_HTTPS=false
|
5
rsconcept/frontend/env/.env.production
vendored
Normal file
5
rsconcept/frontend/env/.env.production
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Frontend public settings: Production
|
||||||
|
|
||||||
|
VITE_PORTAL_BACKEND=https://portal.acconcept.ru:8082
|
||||||
|
VITE_PORTAL_FRONT_PORT=3000
|
||||||
|
VITE_PORTAL_FRONT_HTTPS=true
|
6
rsconcept/frontend/env/.env.production.local
vendored
Normal file
6
rsconcept/frontend/env/.env.production.local
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Frontend public settings: Production Local
|
||||||
|
|
||||||
|
VITE_PORTAL_BACKEND=https://localhost:8001
|
||||||
|
VITE_PORTAL_FRONT_PORT=3001
|
||||||
|
VITE_PORTAL_FRONT_HTTPS=true
|
||||||
|
|
|
@ -18,7 +18,7 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full
|
||||||
|
|
||||||
const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer';
|
const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer';
|
||||||
|
|
||||||
function handleLabelClick(event: React.MouseEvent<HTMLLabelElement, MouseEvent>): void {
|
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
inputRef.current?.click();
|
inputRef.current?.click();
|
||||||
|
@ -26,7 +26,12 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass} title={tooltip}>
|
<button
|
||||||
|
className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass}
|
||||||
|
title={tooltip}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
<input id={id} type='checkbox' ref={inputRef}
|
<input id={id} type='checkbox' ref={inputRef}
|
||||||
className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox ${cursor}`}
|
className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox ${cursor}`}
|
||||||
required={required}
|
required={required}
|
||||||
|
@ -40,7 +45,6 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full
|
||||||
text={label}
|
text={label}
|
||||||
required={required}
|
required={required}
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
onClick={handleLabelClick}
|
|
||||||
/>}
|
/>}
|
||||||
<svg
|
<svg
|
||||||
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
|
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
|
||||||
|
@ -49,7 +53,7 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full
|
||||||
>
|
>
|
||||||
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface DropdownProps {
|
||||||
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
|
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
|
||||||
return (
|
return (
|
||||||
<div className='relative text-sm'>
|
<div className='relative text-sm'>
|
||||||
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-2 z-40 flex flex-col items-stretch justify-start origin-top-right border divide-y rounded-md shadow-lg clr-input clr-border ${widthClass}`}>
|
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-2 py-1 z-40 flex flex-col items-stretch justify-start origin-top-right border divide-y rounded-md shadow-lg clr-input clr-border ${widthClass}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
interface NavigationTextItemProps {
|
interface DropdownButtonProps {
|
||||||
description?: string | undefined
|
tooltip?: string | undefined
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownButton({ description = '', onClick, disabled, children }: NavigationTextItemProps) {
|
function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) {
|
||||||
const behavior = (onClick ? 'cursor-pointer clr-hover' : 'cursor-default') + ' disabled:cursor-not-allowed';
|
const behavior = (onClick ? 'cursor-pointer disabled:cursor-not-allowed clr-hover' : 'cursor-default');
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={description}
|
title={tooltip}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`px-3 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
|
className={`px-3 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
|
|
||||||
|
interface DropdownCheckboxProps {
|
||||||
|
label?: string
|
||||||
|
tooltip?: string
|
||||||
|
disabled?: boolean
|
||||||
|
value?: boolean
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownCheckboxProps) {
|
||||||
|
const behavior = (onChange && !disabled ? 'clr-hover' : '');
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
title={tooltip}
|
||||||
|
className={`px-4 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
widthClass='w-fit'
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropdownCheckbox;
|
|
@ -4,7 +4,7 @@ function HelpRSFormMeta() {
|
||||||
<h1>Паспорт схемы</h1>
|
<h1>Паспорт схемы</h1>
|
||||||
<p><b>Владелец</b> - пользователь, обладающий правом редактирования</p>
|
<p><b>Владелец</b> - пользователь, обладающий правом редактирования</p>
|
||||||
<p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p>
|
<p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p>
|
||||||
<p>Для <b>библиотечных</b> схем правом редактирования обладают только администраторы</p>
|
<p>Для <b>не</b> схем правом редактирования обладают только администраторы</p>
|
||||||
<p><b>Клонировать</b> - создать копию схемы для дальнейшего редактирования</p>
|
<p><b>Клонировать</b> - создать копию схемы для дальнейшего редактирования</p>
|
||||||
<p><b>Отслеживание</b> - возможность видеть схему в Библиотеке и использовать фильтры</p>
|
<p><b>Отслеживание</b> - возможность видеть схему в Библиотеке и использовать фильтры</p>
|
||||||
<p><b>Загрузить/Выгрузить схему</b> - взаимодействие с Экстеор через файлы формата TRS</p>
|
<p><b>Загрузить/Выгрузить схему</b> - взаимодействие с Экстеор через файлы формата TRS</p>
|
||||||
|
|
|
@ -11,9 +11,9 @@ function InfoConstituenta({ data, ...props }: InfoConstituentaProps) {
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<h1>Конституента {data.alias}</h1>
|
<h1>Конституента {data.alias}</h1>
|
||||||
<p><b>Типизация: </b>{getCstTypificationLabel(data)}</p>
|
<p><b>Типизация: </b>{getCstTypificationLabel(data)}</p>
|
||||||
<p><b>Термин: </b>{data.term.resolved || data.term.raw}</p>
|
<p><b>Термин: </b>{data.term_resolved || data.term_raw}</p>
|
||||||
{data.definition.formal && <p><b>Выражение: </b>{data.definition.formal}</p>}
|
{data.definition_formal && <p><b>Выражение: </b>{data.definition_formal}</p>}
|
||||||
{data.definition.text.resolved && <p><b>Определение: </b>{data.definition.text.resolved}</p>}
|
{data.definition_resolved && <p><b>Определение: </b>{data.definition_resolved}</p>}
|
||||||
{data.convention && <p><b>Конвенция: </b>{data.convention}</p>}
|
{data.convention && <p><b>Конвенция: </b>{data.convention}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,13 +34,13 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
|
||||||
return (
|
return (
|
||||||
<Dropdown widthClass='w-36' stretchLeft>
|
<Dropdown widthClass='w-36' stretchLeft>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
description='Профиль пользователя'
|
tooltip='Профиль пользователя'
|
||||||
onClick={navigateProfile}
|
onClick={navigateProfile}
|
||||||
>
|
>
|
||||||
{user?.username}
|
{user?.username}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
description='Переключение темы оформления'
|
tooltip='Переключение темы оформления'
|
||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
>
|
>
|
||||||
{darkMode ? 'Светлая тема' : 'Темная тема'}
|
{darkMode ? 'Светлая тема' : 'Темная тема'}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Label from '../Common/Label';
|
||||||
import { ccBracketMatching } from './bracketMatching';
|
import { ccBracketMatching } from './bracketMatching';
|
||||||
import { RSLanguage } from './rslang';
|
import { RSLanguage } from './rslang';
|
||||||
import { getSymbolSubstitute,TextWrapper } from './textEditing';
|
import { getSymbolSubstitute,TextWrapper } from './textEditing';
|
||||||
import { rshoverTooltip } from './tooltip';
|
import { rshoverTooltip as rsHoverTooltip } from './tooltip';
|
||||||
|
|
||||||
const editorSetup: BasicSetupOptions = {
|
const editorSetup: BasicSetupOptions = {
|
||||||
highlightSpecialChars: false,
|
highlightSpecialChars: false,
|
||||||
|
@ -111,7 +111,7 @@ function RSInput({
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
RSLanguage,
|
RSLanguage,
|
||||||
ccBracketMatching(darkMode),
|
ccBracketMatching(darkMode),
|
||||||
rshoverTooltip(schema?.items || []),
|
rsHoverTooltip(schema?.items || []),
|
||||||
], [darkMode, schema?.items]);
|
], [darkMode, schema?.items]);
|
||||||
|
|
||||||
const handleInput = useCallback(
|
const handleInput = useCallback(
|
||||||
|
|
|
@ -6,24 +6,23 @@ import { getCstTypificationLabel } from '../../utils/staticUI';
|
||||||
|
|
||||||
function createTooltipFor(cst: IConstituenta) {
|
function createTooltipFor(cst: IConstituenta) {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = 'overflow-y-auto border shadow-md max-h-[25rem] max-w-[25rem] min-w-[10rem] w-fit z-20 text-sm';
|
dom.className = 'overflow-y-auto border shadow-md max-h-[25rem] max-w-[25rem] min-w-[10rem] w-fit z-20 text-sm clr-border px-2 py-2';
|
||||||
const alias = document.createElement('h1');
|
const alias = document.createElement('p');
|
||||||
alias.className = 'text-sm text-left';
|
alias.innerHTML = `<b>${cst.alias}:</b> ${getCstTypificationLabel(cst)}`;
|
||||||
alias.textContent = `${cst.alias}: ${getCstTypificationLabel(cst)}`;
|
|
||||||
dom.appendChild(alias);
|
dom.appendChild(alias);
|
||||||
if (cst.term.resolved) {
|
if (cst.term_resolved) {
|
||||||
const term = document.createElement('p');
|
const term = document.createElement('p');
|
||||||
term.innerHTML = `<b>Термин:</b> ${cst.term.resolved}`;
|
term.innerHTML = `<b>Термин:</b> ${cst.term_resolved}`;
|
||||||
dom.appendChild(term);
|
dom.appendChild(term);
|
||||||
}
|
}
|
||||||
if (cst.definition.formal) {
|
if (cst.definition_formal) {
|
||||||
const expression = document.createElement('p');
|
const expression = document.createElement('p');
|
||||||
expression.innerHTML = `<b>Выражение:</b> ${cst.definition.formal}`;
|
expression.innerHTML = `<b>Выражение:</b> ${cst.definition_formal}`;
|
||||||
dom.appendChild(expression);
|
dom.appendChild(expression);
|
||||||
}
|
}
|
||||||
if (cst.definition.text.resolved) {
|
if (cst.definition_resolved) {
|
||||||
const definition = document.createElement('p');
|
const definition = document.createElement('p');
|
||||||
definition.innerHTML = `<b>Определение:</b> ${cst.definition.text.resolved}`;
|
definition.innerHTML = `<b>Определение:</b> ${cst.definition_resolved}`;
|
||||||
dom.appendChild(definition);
|
dom.appendChild(definition);
|
||||||
}
|
}
|
||||||
if (cst.convention) {
|
if (cst.convention) {
|
||||||
|
|
|
@ -58,14 +58,14 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
||||||
const mainHeight = useMemo(
|
const mainHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 8rem)'
|
'calc(100vh - 7rem - 2px)'
|
||||||
: '100vh';
|
: '100vh';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
const viewportHeight = useMemo(
|
const viewportHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 3.9rem)'
|
'calc(100vh - 3rem - 2px)'
|
||||||
: '100vh';
|
: '100vh';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) {
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: parse => {
|
onSuccess: parse => {
|
||||||
if (activeCst) {
|
if (activeCst) {
|
||||||
adjustResults(parse, expression === getCstExpressionPrefix(activeCst), activeCst.cstType);
|
adjustResults(parse, expression === getCstExpressionPrefix(activeCst), activeCst.cst_type);
|
||||||
}
|
}
|
||||||
setParseData(parse);
|
setParseData(parse);
|
||||||
if (onSuccess) onSuccess(parse);
|
if (onSuccess) onSuccess(parse);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import BackendError from '../components/BackendError';
|
import BackendError from '../components/BackendError';
|
||||||
|
import Button from '../components/Common/Button';
|
||||||
import Checkbox from '../components/Common/Checkbox';
|
import Checkbox from '../components/Common/Checkbox';
|
||||||
import FileInput from '../components/Common/FileInput';
|
import FileInput from '../components/Common/FileInput';
|
||||||
import Form from '../components/Common/Form';
|
import Form from '../components/Common/Form';
|
||||||
|
@ -14,6 +15,7 @@ import { useLibrary } from '../context/LibraryContext';
|
||||||
import { IRSFormCreateData, LibraryItemType } from '../utils/models';
|
import { IRSFormCreateData, LibraryItemType } from '../utils/models';
|
||||||
|
|
||||||
function CreateRSFormPage() {
|
function CreateRSFormPage() {
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createSchema, error, setError, processing } = useLibrary();
|
const { createSchema, error, setError, processing } = useLibrary();
|
||||||
|
|
||||||
|
@ -35,6 +37,14 @@ function CreateRSFormPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
if (location.key !== "default") {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate('/library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (processing) {
|
if (processing) {
|
||||||
|
@ -89,10 +99,16 @@ function CreateRSFormPage() {
|
||||||
onChange={handleFile}
|
onChange={handleFile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex items-center justify-center py-2 mt-4'>
|
<div className='flex items-center justify-center gap-4 py-2 mt-4'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Создать схему'
|
text='Создать схему'
|
||||||
loading={processing}
|
loading={processing}
|
||||||
|
widthClass='min-w-[10rem]'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text='Отмена'
|
||||||
|
onClick={() => handleCancel()}
|
||||||
|
widthClass='min-w-[10rem]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ error && <BackendError error={error} />}
|
{ error && <BackendError error={error} />}
|
||||||
|
|
|
@ -13,7 +13,7 @@ function HomePage() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/manuals');
|
navigate('/manuals');
|
||||||
}, TIMEOUT_UI_REFRESH);
|
}, TIMEOUT_UI_REFRESH);
|
||||||
} else if(!user.is_staff) {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/library');
|
navigate('/library');
|
||||||
}, TIMEOUT_UI_REFRESH);
|
}, TIMEOUT_UI_REFRESH);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import Button from '../../components/Common/Button';
|
import Button from '../../components/Common/Button';
|
||||||
import Checkbox from '../../components/Common/Checkbox';
|
|
||||||
import Dropdown from '../../components/Common/Dropdown';
|
import Dropdown from '../../components/Common/Dropdown';
|
||||||
import DropdownButton from '../../components/Common/DropdownButton';
|
import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
|
||||||
import { FilterCogIcon } from '../../components/Icons';
|
import { FilterCogIcon } from '../../components/Icons';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import useDropdown from '../../hooks/useDropdown';
|
import useDropdown from '../../hooks/useDropdown';
|
||||||
import { LibraryFilterStrategy } from '../../utils/models';
|
import { LibraryFilterStrategy } from '../../utils/models';
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ interface PickerStrategyProps {
|
||||||
|
|
||||||
function PickerStrategy({ value, onChange }: PickerStrategyProps) {
|
function PickerStrategy({ value, onChange }: PickerStrategyProps) {
|
||||||
const pickerMenu = useDropdown();
|
const pickerMenu = useDropdown();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(newValue: LibraryFilterStrategy) => {
|
(newValue: LibraryFilterStrategy) => {
|
||||||
|
@ -34,53 +35,44 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
|
||||||
/>
|
/>
|
||||||
{ pickerMenu.isActive &&
|
{ pickerMenu.isActive &&
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.MANUAL)}>
|
<DropdownCheckbox
|
||||||
<Checkbox
|
onChange={() => handleChange(LibraryFilterStrategy.MANUAL)}
|
||||||
value={value === LibraryFilterStrategy.MANUAL}
|
value={value === LibraryFilterStrategy.MANUAL}
|
||||||
label='Отображать все'
|
label='Отображать все'
|
||||||
widthClass='w-fit px-2'
|
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
<DropdownCheckbox
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.COMMON)}>
|
onChange={() => handleChange(LibraryFilterStrategy.COMMON)}
|
||||||
<Checkbox
|
|
||||||
value={value === LibraryFilterStrategy.COMMON}
|
value={value === LibraryFilterStrategy.COMMON}
|
||||||
label='Общедоступные'
|
label='Общедоступные'
|
||||||
widthClass='w-fit px-2'
|
|
||||||
tooltip='Отображать только общедоступные схемы'
|
tooltip='Отображать только общедоступные схемы'
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
<DropdownCheckbox
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.CANONICAL)}>
|
onChange={() => handleChange(LibraryFilterStrategy.CANONICAL)}
|
||||||
<Checkbox
|
|
||||||
value={value === LibraryFilterStrategy.CANONICAL}
|
value={value === LibraryFilterStrategy.CANONICAL}
|
||||||
label='Библиотечные'
|
label='Неизменные'
|
||||||
widthClass='w-fit px-2'
|
tooltip='Отображать только неизменные схемы'
|
||||||
tooltip='Отображать только библиотечные схемы'
|
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
<DropdownCheckbox
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.PERSONAL)}>
|
onChange={() => handleChange(LibraryFilterStrategy.PERSONAL)}
|
||||||
<Checkbox
|
|
||||||
value={value === LibraryFilterStrategy.PERSONAL}
|
value={value === LibraryFilterStrategy.PERSONAL}
|
||||||
label='Личные'
|
label='Личные'
|
||||||
widthClass='w-fit px-2'
|
disabled={!user}
|
||||||
tooltip='Отображать только подписки и владеемые схемы'
|
tooltip='Отображать только подписки и владеемые схемы'
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
<DropdownCheckbox
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}>
|
onChange={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
|
||||||
<Checkbox
|
|
||||||
value={value === LibraryFilterStrategy.SUBSCRIBE}
|
value={value === LibraryFilterStrategy.SUBSCRIBE}
|
||||||
label='Подписки'
|
label='Подписки'
|
||||||
widthClass='w-fit px-2'
|
disabled={!user}
|
||||||
tooltip='Отображать только подписки'
|
tooltip='Отображать только подписки'
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
<DropdownCheckbox
|
||||||
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.OWNED)}>
|
onChange={() => handleChange(LibraryFilterStrategy.OWNED)}
|
||||||
<Checkbox
|
|
||||||
value={value === LibraryFilterStrategy.OWNED}
|
value={value === LibraryFilterStrategy.OWNED}
|
||||||
|
disabled={!user}
|
||||||
label='Я - Владелец!'
|
label='Я - Владелец!'
|
||||||
widthClass='w-fit px-2'
|
|
||||||
tooltip='Отображать только владеемые схемы'
|
tooltip='Отображать только владеемые схемы'
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
|
||||||
</Dropdown>}
|
</Dropdown>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { MagnifyingGlassIcon } from '../../components/Icons';
|
import { MagnifyingGlassIcon } from '../../components/Icons';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||||
import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
|
import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
|
||||||
import PickerStrategy from './PickerStrategy';
|
import PickerStrategy from './PickerStrategy';
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [strategy, setStrategy] = useState(LibraryFilterStrategy.MANUAL);
|
const [strategy, setStrategy] = useLocalStorage<LibraryFilterStrategy>('search_strategy', LibraryFilterStrategy.MANUAL);
|
||||||
|
|
||||||
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
|
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const newQuery = event.target.value;
|
const newQuery = event.target.value;
|
||||||
|
@ -49,11 +50,15 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null;
|
const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null;
|
||||||
|
if (searchFilter === null) {
|
||||||
|
navigate(`/library?filter=${strategy}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL;
|
const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL;
|
||||||
setQuery('')
|
setQuery('')
|
||||||
setStrategy(inputStrategy)
|
setStrategy(inputStrategy)
|
||||||
setFilter(ApplyStrategy(inputStrategy));
|
setFilter(ApplyStrategy(inputStrategy));
|
||||||
}, [user, search, setQuery, setFilter]);
|
}, [user, search, setQuery, setFilter, setStrategy, strategy, navigate]);
|
||||||
|
|
||||||
const handleChangeStrategy = useCallback(
|
const handleChangeStrategy = useCallback(
|
||||||
(value: LibraryFilterStrategy) => {
|
(value: LibraryFilterStrategy) => {
|
||||||
|
@ -64,7 +69,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
|
||||||
}, [strategy, navigate]);
|
}, [strategy, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-start w-full border-b clr-input'>
|
<div className='sticky top-0 left-0 right-0 z-30 flex items-center justify-start w-full border-b clr-input'>
|
||||||
<div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'>
|
<div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'>
|
||||||
Фильтр
|
Фильтр
|
||||||
<span className='ml-2'>
|
<span className='ml-2'>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { useAuth } from '../context/AuthContext';
|
||||||
import { IUserLoginData } from '../utils/models';
|
import { IUserLoginData } from '../utils/models';
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const search = useLocation().search;
|
const search = useLocation().search;
|
||||||
const { user, login, loading, error, setError } = useAuth();
|
const { user, login, loading, error, setError } = useAuth();
|
||||||
|
@ -34,7 +35,13 @@ function LoginPage() {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password
|
||||||
};
|
};
|
||||||
login(data, () => navigate('/library'));
|
login(data, () => {
|
||||||
|
if (location.key !== "default") {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate('/library');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +51,7 @@ function LoginPage() {
|
||||||
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
||||||
:
|
:
|
||||||
<Form
|
<Form
|
||||||
title='Ввод данных пользователя'
|
title='Вход в Портал'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
widthClass='w-[24rem]'
|
widthClass='w-[24rem]'
|
||||||
>
|
>
|
||||||
|
@ -64,10 +71,10 @@ function LoginPage() {
|
||||||
onChange={event => setPassword(event.target.value)}
|
onChange={event => setPassword(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-center w-full gap-2 mt-4'>
|
<div className='flex justify-center w-full gap-2 py-2 mt-4'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Вход'
|
text='Вход'
|
||||||
widthClass='w-[7rem]'
|
widthClass='w-[12rem]'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||||
|
@ -9,7 +9,6 @@ import TextArea from '../../components/Common/TextArea';
|
||||||
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
||||||
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import useModificationPrompt from '../../hooks/useModificationPrompt';
|
|
||||||
import { CstType, EditMode, ICstCreateData, ICstRenameData, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
import { CstType, EditMode, ICstCreateData, ICstRenameData, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
||||||
import { getCstTypificationLabel } from '../../utils/staticUI';
|
import { getCstTypificationLabel } from '../../utils/staticUI';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import EditorRSExpression from './EditorRSExpression';
|
||||||
|
@ -25,17 +24,20 @@ interface EditorConstituentaProps {
|
||||||
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
||||||
onRenameCst: (initial: ICstRenameData) => void
|
onRenameCst: (initial: ICstRenameData) => void
|
||||||
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
|
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
|
||||||
|
isModified: boolean
|
||||||
|
setIsModified: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst }: EditorConstituentaProps) {
|
function EditorConstituenta({
|
||||||
|
isModified, setIsModified, activeID,
|
||||||
|
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
|
||||||
|
}: EditorConstituentaProps) {
|
||||||
const { schema, processing, isEditable, cstUpdate } = useRSForm();
|
const { schema, processing, isEditable, cstUpdate } = useRSForm();
|
||||||
const activeCst = useMemo(
|
const activeCst = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return schema?.items?.find((cst) => cst.id === activeID);
|
return schema?.items?.find((cst) => cst.id === activeID);
|
||||||
}, [schema?.items, activeID]);
|
}, [schema?.items, activeID]);
|
||||||
|
|
||||||
const { isModified, setIsModified } = useModificationPrompt();
|
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(EditMode.TEXT);
|
const [editMode, setEditMode] = useState(EditMode.TEXT);
|
||||||
|
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
|
@ -54,23 +56,24 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsModified(
|
setIsModified(
|
||||||
activeCst.term.raw !== term ||
|
activeCst.term_raw !== term ||
|
||||||
activeCst.definition.text.raw !== textDefinition ||
|
activeCst.definition_raw !== textDefinition ||
|
||||||
activeCst.convention !== convention ||
|
activeCst.convention !== convention ||
|
||||||
activeCst.definition.formal !== expression
|
activeCst.definition_formal !== expression
|
||||||
);
|
);
|
||||||
}, [activeCst, activeCst?.term, activeCst?.definition.formal,
|
return () => setIsModified(false);
|
||||||
activeCst?.definition.text.raw, activeCst?.convention,
|
}, [activeCst, activeCst?.term_raw, activeCst?.definition_formal,
|
||||||
|
activeCst?.definition_raw, activeCst?.convention,
|
||||||
term, textDefinition, expression, convention, setIsModified]);
|
term, textDefinition, expression, convention, setIsModified]);
|
||||||
|
|
||||||
useLayoutEffect(
|
useLayoutEffect(
|
||||||
() => {
|
() => {
|
||||||
if (activeCst) {
|
if (activeCst) {
|
||||||
setAlias(activeCst.alias);
|
setAlias(activeCst.alias);
|
||||||
setConvention(activeCst.convention ?? '');
|
setConvention(activeCst.convention || '');
|
||||||
setTerm(activeCst.term?.raw ?? '');
|
setTerm(activeCst.term_raw || '');
|
||||||
setTextDefinition(activeCst.definition?.text?.raw ?? '');
|
setTextDefinition(activeCst.definition_raw || '');
|
||||||
setExpression(activeCst.definition?.formal ?? '');
|
setExpression(activeCst.definition_formal || '');
|
||||||
setTypification(activeCst ? getCstTypificationLabel(activeCst) : 'N/A');
|
setTypification(activeCst ? getCstTypificationLabel(activeCst) : 'N/A');
|
||||||
}
|
}
|
||||||
}, [activeCst, onOpenEdit, schema]);
|
}, [activeCst, onOpenEdit, schema]);
|
||||||
|
@ -106,7 +109,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
||||||
}
|
}
|
||||||
const data: ICstCreateData = {
|
const data: ICstCreateData = {
|
||||||
insert_after: activeID,
|
insert_after: activeID,
|
||||||
cst_type: activeCst?.cstType ?? CstType.BASE,
|
cst_type: activeCst?.cst_type ?? CstType.BASE,
|
||||||
alias: '',
|
alias: '',
|
||||||
term_raw: '',
|
term_raw: '',
|
||||||
definition_formal: '',
|
definition_formal: '',
|
||||||
|
@ -123,7 +126,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
||||||
const data: ICstRenameData = {
|
const data: ICstRenameData = {
|
||||||
id: activeID,
|
id: activeID,
|
||||||
alias: activeCst?.alias,
|
alias: activeCst?.alias,
|
||||||
cst_type: activeCst.cstType
|
cst_type: activeCst.cst_type
|
||||||
};
|
};
|
||||||
onRenameCst(data);
|
onRenameCst(data);
|
||||||
}
|
}
|
||||||
|
@ -181,8 +184,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
||||||
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
|
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
|
||||||
rows={2}
|
rows={2}
|
||||||
value={term}
|
value={term}
|
||||||
initialValue={activeCst?.term.raw ?? ''}
|
initialValue={activeCst?.term_raw ?? ''}
|
||||||
resolved={activeCst?.term.resolved ?? ''}
|
resolved={activeCst?.term_resolved ?? ''}
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
spellCheck
|
spellCheck
|
||||||
onChange={event => setTerm(event.target.value)}
|
onChange={event => setTerm(event.target.value)}
|
||||||
|
@ -209,8 +212,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
||||||
placeholder='Лингвистическая интерпретация формального выражения'
|
placeholder='Лингвистическая интерпретация формального выражения'
|
||||||
rows={4}
|
rows={4}
|
||||||
value={textDefinition}
|
value={textDefinition}
|
||||||
initialValue={activeCst?.definition.text.raw ?? ''}
|
initialValue={activeCst?.definition_raw ?? ''}
|
||||||
resolved={activeCst?.definition.text.resolved ?? ''}
|
resolved={activeCst?.definition_resolved ?? ''}
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
spellCheck
|
spellCheck
|
||||||
onChange={event => setTextDefinition(event.target.value)}
|
onChange={event => setTextDefinition(event.target.value)}
|
||||||
|
|
|
@ -215,7 +215,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
{
|
{
|
||||||
name: 'Термин',
|
name: 'Термин',
|
||||||
id: 'term',
|
id: 'term',
|
||||||
selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '',
|
selector: (cst: IConstituenta) => cst.term_resolved || cst.term_raw || '',
|
||||||
width: '350px',
|
width: '350px',
|
||||||
minWidth: '150px',
|
minWidth: '150px',
|
||||||
maxWidth: '350px',
|
maxWidth: '350px',
|
||||||
|
@ -225,7 +225,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
{
|
{
|
||||||
name: 'Формальное определение',
|
name: 'Формальное определение',
|
||||||
id: 'expression',
|
id: 'expression',
|
||||||
selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
|
selector: (cst: IConstituenta) => cst.definition_formal || '',
|
||||||
minWidth: '300px',
|
minWidth: '300px',
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
grow: 2,
|
grow: 2,
|
||||||
|
@ -237,7 +237,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
id: 'definition',
|
id: 'definition',
|
||||||
cell: (cst: IConstituenta) => (
|
cell: (cst: IConstituenta) => (
|
||||||
<div style={{ fontSize: 12 }}>
|
<div style={{ fontSize: 12 }}>
|
||||||
{cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
|
{cst.definition_resolved || cst.definition_raw || ''}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
|
|
|
@ -225,6 +225,12 @@ function EditorRSExpression({
|
||||||
onShowAST={ast => onShowAST(value, ast)}
|
onShowAST={ast => onShowAST(value, ast)}
|
||||||
onShowError={onShowError}
|
onShowError={onShowError}
|
||||||
/>}
|
/>}
|
||||||
|
{ !loading && !parseData &&
|
||||||
|
<input
|
||||||
|
disabled={true}
|
||||||
|
className='w-full h-full px-2 align-middle select-none clr-app'
|
||||||
|
placeholder='Результаты проверки выражения'
|
||||||
|
/>}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import { CrownIcon, DownloadIcon, DumpBinIcon, HelpIcon, SaveIcon, ShareIcon } f
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { useUsers } from '../../context/UsersContext';
|
import { useUsers } from '../../context/UsersContext';
|
||||||
import useModificationPrompt from '../../hooks/useModificationPrompt';
|
|
||||||
import { IRSFormCreateData, LibraryItemType } from '../../utils/models';
|
import { IRSFormCreateData, LibraryItemType } from '../../utils/models';
|
||||||
|
|
||||||
interface EditorRSFormProps {
|
interface EditorRSFormProps {
|
||||||
|
@ -21,9 +20,11 @@ interface EditorRSFormProps {
|
||||||
onClaim: () => void
|
onClaim: () => void
|
||||||
onShare: () => void
|
onShare: () => void
|
||||||
onDownload: () => void
|
onDownload: () => void
|
||||||
|
isModified: boolean
|
||||||
|
setIsModified: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormProps) {
|
function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified, onDownload }: EditorRSFormProps) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getUserLabel } = useUsers();
|
const { getUserLabel } = useUsers();
|
||||||
const {
|
const {
|
||||||
|
@ -38,8 +39,6 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
|
||||||
const [common, setCommon] = useState(false);
|
const [common, setCommon] = useState(false);
|
||||||
const [canonical, setCanonical] = useState(false);
|
const [canonical, setCanonical] = useState(false);
|
||||||
|
|
||||||
const { isModified, setIsModified } = useModificationPrompt();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
|
@ -52,6 +51,7 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
|
||||||
schema.is_common !== common ||
|
schema.is_common !== common ||
|
||||||
schema.is_canonical !== canonical
|
schema.is_canonical !== canonical
|
||||||
);
|
);
|
||||||
|
return () => setIsModified(false);
|
||||||
}, [schema, schema?.title, schema?.alias, schema?.comment,
|
}, [schema, schema?.title, schema?.alias, schema?.comment,
|
||||||
schema?.is_common, schema?.is_canonical,
|
schema?.is_common, schema?.is_canonical,
|
||||||
title, alias, comment, common, canonical, setIsModified]);
|
title, alias, comment, common, canonical, setIsModified]);
|
||||||
|
@ -138,10 +138,10 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
onChange={event => setCommon(event.target.checked)}
|
onChange={event => setCommon(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
<Checkbox id='canonical' label='Неизменяемая схема'
|
<Checkbox id='canonical' label='Неизменная схема'
|
||||||
widthClass='w-fit'
|
widthClass='w-fit'
|
||||||
value={canonical}
|
value={canonical}
|
||||||
tooltip='Только администраторы могут присваивать схемам библиотечный статус'
|
tooltip='Только администраторы могут присваивать схемам неизменный статус'
|
||||||
disabled={!isEditable || !isForceAdmin}
|
disabled={!isEditable || !isForceAdmin}
|
||||||
onChange={event => setCanonical(event.target.checked)}
|
onChange={event => setCanonical(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -31,7 +31,7 @@ const TREE_SIZE_MILESTONE = 50;
|
||||||
|
|
||||||
function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, colors: IColorTheme): string {
|
function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, colors: IColorTheme): string {
|
||||||
if (coloringScheme === 'type') {
|
if (coloringScheme === 'type') {
|
||||||
return getCstClassColor(cst.cstClass, colors);
|
return getCstClassColor(cst.cst_class, colors);
|
||||||
}
|
}
|
||||||
if (coloringScheme === 'status') {
|
if (coloringScheme === 'status') {
|
||||||
return getCstStatusColor(cst.status, colors);
|
return getCstStatusColor(cst.status, colors);
|
||||||
|
@ -125,14 +125,14 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
}
|
}
|
||||||
if (noTemplates) {
|
if (noTemplates) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (cst.isTemplate) {
|
if (cst.is_template) {
|
||||||
graph.foldNode(cst.id);
|
graph.foldNode(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (allowedTypes.length < Object.values(CstType).length) {
|
if (allowedTypes.length < Object.values(CstType).length) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (!allowedTypes.includes(cst.cstType)) {
|
if (!allowedTypes.includes(cst.cst_type)) {
|
||||||
graph.foldNode(cst.id);
|
graph.foldNode(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -173,7 +173,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
result.push({
|
result.push({
|
||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
fill: getCstNodeColor(cst, coloringScheme, colors),
|
fill: getCstNodeColor(cst, coloringScheme, colors),
|
||||||
label: cst.term.resolved && !noTerms ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
|
label: cst.term_resolved && !noTerms ? `${cst.alias}: ${cst.term_resolved}` : cst.alias
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -338,8 +338,8 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
const canvasHeight = useMemo(
|
const canvasHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 10.1rem)'
|
'calc(100vh - 9.8rem - 4px)'
|
||||||
: 'calc(100vh - 2.1rem)';
|
: 'calc(100vh - 3rem - 4px)';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
const dismissedStyle = useCallback(
|
const dismissedStyle = useCallback(
|
||||||
|
@ -360,7 +360,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<InfoConstituenta
|
<InfoConstituenta
|
||||||
data={hoverCst}
|
data={hoverCst}
|
||||||
className='absolute top-0 left-0 z-50 w-[25rem] min-h-[11rem] overflow-y-auto border h-fit clr-app px-3'
|
className='absolute top-[2.2rem] left-[2.6rem] z-50 w-[25rem] min-h-[11rem] overflow-y-auto border h-fit clr-app px-3'
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
@ -460,7 +460,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-full overflow-auto'>
|
<div className='w-full h-full overflow-auto border'>
|
||||||
<div
|
<div
|
||||||
className='relative'
|
className='relative'
|
||||||
style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}}
|
style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}}
|
||||||
|
|
|
@ -10,8 +10,9 @@ import { Loader } from '../../components/Common/Loader';
|
||||||
import { useLibrary } from '../../context/LibraryContext';
|
import { useLibrary } from '../../context/LibraryContext';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
import useModificationPrompt from '../../hooks/useModificationPrompt';
|
||||||
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
|
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
|
||||||
import { ICstCreateData, ICstRenameData, LibraryFilterStrategy, SyntaxTree } from '../../utils/models';
|
import { ICstCreateData, ICstRenameData, SyntaxTree } from '../../utils/models';
|
||||||
import { createAliasFor } from '../../utils/staticUI';
|
import { createAliasFor } from '../../utils/staticUI';
|
||||||
import DlgCloneRSForm from './DlgCloneRSForm';
|
import DlgCloneRSForm from './DlgCloneRSForm';
|
||||||
import DlgCreateCst from './DlgCreateCst';
|
import DlgCreateCst from './DlgCreateCst';
|
||||||
|
@ -43,6 +44,8 @@ function RSTabs() {
|
||||||
const { destroySchema } = useLibrary();
|
const { destroySchema } = useLibrary();
|
||||||
const { setNoFooter } = useConceptTheme();
|
const { setNoFooter } = useConceptTheme();
|
||||||
|
|
||||||
|
const { isModified, setIsModified } = useModificationPrompt();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<RSTabID>(RSTabID.CARD);
|
const [activeTab, setActiveTab] = useState<RSTabID>(RSTabID.CARD);
|
||||||
const [activeID, setActiveID] = useState<number | undefined>(undefined);
|
const [activeID, setActiveID] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -204,7 +207,7 @@ function RSTabs() {
|
||||||
}
|
}
|
||||||
destroySchema(schema.id, () => {
|
destroySchema(schema.id, () => {
|
||||||
toast.success('Схема удалена');
|
toast.success('Схема удалена');
|
||||||
navigate(`/library?filter=${LibraryFilterStrategy.PERSONAL}`);
|
navigate('/library');
|
||||||
});
|
});
|
||||||
}, [schema, destroySchema, navigate]);
|
}, [schema, destroySchema, navigate]);
|
||||||
|
|
||||||
|
@ -226,6 +229,11 @@ function RSTabs() {
|
||||||
|
|
||||||
const onDownloadSchema = useCallback(
|
const onDownloadSchema = useCallback(
|
||||||
() => {
|
() => {
|
||||||
|
if (isModified) {
|
||||||
|
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const fileName = (schema?.alias ?? 'Schema') + '.trs';
|
const fileName = (schema?.alias ?? 'Schema') + '.trs';
|
||||||
download(
|
download(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -235,7 +243,17 @@ function RSTabs() {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schema?.alias, download]);
|
}, [schema?.alias, download, isModified]);
|
||||||
|
|
||||||
|
const handleShowClone = useCallback(
|
||||||
|
() => {
|
||||||
|
if (isModified) {
|
||||||
|
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowClone(true);
|
||||||
|
}, [isModified]);
|
||||||
|
|
||||||
const handleToggleSubscribe = useCallback(
|
const handleToggleSubscribe = useCallback(
|
||||||
() => {
|
() => {
|
||||||
|
@ -302,7 +320,7 @@ function RSTabs() {
|
||||||
onClaim={onClaimSchema}
|
onClaim={onClaimSchema}
|
||||||
onShare={onShareSchema}
|
onShare={onShareSchema}
|
||||||
onToggleSubscribe={handleToggleSubscribe}
|
onToggleSubscribe={handleToggleSubscribe}
|
||||||
showCloneDialog={() => setShowClone(true)}
|
showCloneDialog={handleShowClone}
|
||||||
showUploadDialog={() => setShowUpload(true)}
|
showUploadDialog={() => setShowUpload(true)}
|
||||||
/>
|
/>
|
||||||
<ConceptTab className='border-r-2 min-w-[7.8rem]'>Паспорт схемы</ConceptTab>
|
<ConceptTab className='border-r-2 min-w-[7.8rem]'>Паспорт схемы</ConceptTab>
|
||||||
|
@ -316,6 +334,8 @@ function RSTabs() {
|
||||||
|
|
||||||
<TabPanel className='flex w-full gap-4'>
|
<TabPanel className='flex w-full gap-4'>
|
||||||
<EditorRSForm
|
<EditorRSForm
|
||||||
|
isModified={isModified}
|
||||||
|
setIsModified={setIsModified}
|
||||||
onDownload={onDownloadSchema}
|
onDownload={onDownloadSchema}
|
||||||
onDestroy={onDestroySchema}
|
onDestroy={onDestroySchema}
|
||||||
onClaim={onClaimSchema}
|
onClaim={onClaimSchema}
|
||||||
|
@ -334,6 +354,8 @@ function RSTabs() {
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorConstituenta
|
<EditorConstituenta
|
||||||
|
isModified={isModified}
|
||||||
|
setIsModified={setIsModified}
|
||||||
activeID={activeID}
|
activeID={activeID}
|
||||||
onOpenEdit={onOpenCst}
|
onOpenEdit={onOpenCst}
|
||||||
onShowAST={onShowAST}
|
onShowAST={onShowAST}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import Button from '../../components/Common/Button';
|
import Button from '../../components/Common/Button';
|
||||||
import Checkbox from '../../components/Common/Checkbox';
|
|
||||||
import Dropdown from '../../components/Common/Dropdown';
|
import Dropdown from '../../components/Common/Dropdown';
|
||||||
import DropdownButton from '../../components/Common/DropdownButton';
|
import DropdownButton from '../../components/Common/DropdownButton';
|
||||||
|
import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
|
||||||
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, PlusIcon, ShareIcon, UploadIcon } from '../../components/Icons';
|
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, PlusIcon, ShareIcon, UploadIcon } from '../../components/Icons';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
|
@ -131,7 +131,7 @@ function RSTabsMenu({
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
disabled={!user || !isClaimable}
|
disabled={!user || !isClaimable}
|
||||||
onClick={!isOwned ? handleClaimOwner : undefined}
|
onClick={!isOwned ? handleClaimOwner : undefined}
|
||||||
description={!user || !isClaimable ? 'Стать владельцем можно только для общей небиблиотечной схемы' : ''}
|
tooltip={!user || !isClaimable ? 'Стать владельцем можно только для общей изменяемой схемы' : ''}
|
||||||
>
|
>
|
||||||
<div className='inline-flex items-center gap-1 justify-normal'>
|
<div className='inline-flex items-center gap-1 justify-normal'>
|
||||||
<span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span>
|
<span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span>
|
||||||
|
@ -142,17 +142,18 @@ function RSTabsMenu({
|
||||||
</div>
|
</div>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
{(isOwned || user?.is_staff) &&
|
{(isOwned || user?.is_staff) &&
|
||||||
<DropdownButton onClick={toggleReadonly}>
|
<DropdownCheckbox
|
||||||
<Checkbox
|
|
||||||
value={isReadonly}
|
value={isReadonly}
|
||||||
|
onChange={toggleReadonly}
|
||||||
label='Я — читатель!'
|
label='Я — читатель!'
|
||||||
tooltip='Режим чтения'
|
tooltip='Режим чтения'
|
||||||
/>
|
/>}
|
||||||
</DropdownButton>}
|
|
||||||
{user?.is_staff &&
|
{user?.is_staff &&
|
||||||
<DropdownButton onClick={toggleForceAdmin}>
|
<DropdownCheckbox
|
||||||
<Checkbox value={isForceAdmin} label='режим администратора'/>
|
value={isForceAdmin}
|
||||||
</DropdownButton>}
|
onChange={toggleForceAdmin}
|
||||||
|
label='режим администратора'
|
||||||
|
/>}
|
||||||
</Dropdown>}
|
</Dropdown>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -49,7 +49,16 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
const diff = Array.from(aliases).filter(name => !names.includes(name));
|
const diff = Array.from(aliases).filter(name => !names.includes(name));
|
||||||
if (diff.length > 0) {
|
if (diff.length > 0) {
|
||||||
diff.forEach(
|
diff.forEach(
|
||||||
(alias, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
|
(alias, index) => filtered.push(
|
||||||
|
getMockConstituenta(
|
||||||
|
schema.id,
|
||||||
|
-index,
|
||||||
|
alias,
|
||||||
|
CstType.BASE,
|
||||||
|
'Конституента отсутствует'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (!activeID) {
|
} else if (!activeID) {
|
||||||
filtered = schema.items
|
filtered = schema.items
|
||||||
|
@ -133,7 +142,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
{
|
{
|
||||||
name: 'Выражение',
|
name: 'Выражение',
|
||||||
id: 'expression',
|
id: 'expression',
|
||||||
selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
|
selector: (cst: IConstituenta) => cst.definition_formal || '',
|
||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
hide: 1600,
|
hide: 1600,
|
||||||
grow: 2,
|
grow: 2,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import BackendError from '../components/BackendError';
|
import BackendError from '../components/BackendError';
|
||||||
|
import Button from '../components/Common/Button';
|
||||||
import Form from '../components/Common/Form';
|
import Form from '../components/Common/Form';
|
||||||
import SubmitButton from '../components/Common/SubmitButton';
|
import SubmitButton from '../components/Common/SubmitButton';
|
||||||
import TextInput from '../components/Common/TextInput';
|
import TextInput from '../components/Common/TextInput';
|
||||||
|
@ -10,6 +11,7 @@ import { useAuth } from '../context/AuthContext';
|
||||||
import { type IUserSignupData } from '../utils/models';
|
import { type IUserSignupData } from '../utils/models';
|
||||||
|
|
||||||
function RegisterPage() {
|
function RegisterPage() {
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, signup, loading, error, setError } = useAuth();
|
const { user, signup, loading, error, setError } = useAuth();
|
||||||
|
|
||||||
|
@ -24,6 +26,14 @@ function RegisterPage() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
}, [username, email, password, password2, setError]);
|
}, [username, email, password, password2, setError]);
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
if (location.key !== "default") {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate('/library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
@ -48,7 +58,7 @@ function RegisterPage() {
|
||||||
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
|
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
|
||||||
{ !user &&
|
{ !user &&
|
||||||
<Form
|
<Form
|
||||||
title='Регистрация пользователя'
|
title='Регистрация'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
widthClass='w-[24rem]'
|
widthClass='w-[24rem]'
|
||||||
>
|
>
|
||||||
|
@ -89,8 +99,17 @@ function RegisterPage() {
|
||||||
onChange={event => setLastName(event.target.value)}
|
onChange={event => setLastName(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex items-center justify-center w-full my-4'>
|
<div className='flex items-center justify-center w-full gap-4 my-4'>
|
||||||
<SubmitButton text='Регистрировать' loading={loading}/>
|
<SubmitButton
|
||||||
|
text='Регистрировать'
|
||||||
|
loading={loading}
|
||||||
|
widthClass='min-w-[10rem]'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text='Отмена'
|
||||||
|
onClick={() => handleCancel()}
|
||||||
|
widthClass='min-w-[10rem]'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ error && <BackendError error={error} />}
|
{ error && <BackendError error={error} />}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
// Constants
|
// Constants
|
||||||
const prod = {
|
export const config = {
|
||||||
backend: 'https://portal.acconcept.ru:8082',
|
backend: import.meta.env.VITE_PORTAL_BACKEND as string
|
||||||
// backend: 'https://dev.concept.ru:8000',
|
|
||||||
// backend: 'https://localhost:8000',
|
|
||||||
// backend: 'https://api.portal.concept.ru',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dev = {
|
|
||||||
backend: 'http://localhost:8000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const config = process.env.NODE_ENV === 'production' ? prod : dev;
|
|
||||||
export const TIMEOUT_UI_REFRESH = 100;
|
export const TIMEOUT_UI_REFRESH = 100;
|
||||||
|
|
||||||
export const youtube = {
|
export const youtube = {
|
||||||
|
|
|
@ -148,33 +148,9 @@ export enum CstClass {
|
||||||
TEMPLATE = 'template'
|
TEMPLATE = 'template'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConstituenta {
|
export interface TermForm {
|
||||||
id: number
|
text: string
|
||||||
alias: string
|
tags: string
|
||||||
cstType: CstType
|
|
||||||
convention: string
|
|
||||||
term: {
|
|
||||||
raw: string
|
|
||||||
resolved: string
|
|
||||||
forms: string[]
|
|
||||||
}
|
|
||||||
definition: {
|
|
||||||
formal: string
|
|
||||||
text: {
|
|
||||||
raw: string
|
|
||||||
resolved: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cstClass: CstClass
|
|
||||||
status: ExpressionStatus
|
|
||||||
isTemplate: boolean
|
|
||||||
parse: {
|
|
||||||
status: ParsingStatus
|
|
||||||
valueClass: ValueClass
|
|
||||||
typification: string
|
|
||||||
syntaxTree: string
|
|
||||||
args: IFunctionArg[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConstituentaMeta {
|
export interface IConstituentaMeta {
|
||||||
|
@ -189,6 +165,21 @@ export interface IConstituentaMeta {
|
||||||
definition_resolved: string
|
definition_resolved: string
|
||||||
term_raw: string
|
term_raw: string
|
||||||
term_resolved: string
|
term_resolved: string
|
||||||
|
term_forms: TermForm[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConstituenta
|
||||||
|
extends IConstituentaMeta {
|
||||||
|
cst_class: CstClass
|
||||||
|
status: ExpressionStatus
|
||||||
|
is_template: boolean
|
||||||
|
parse: {
|
||||||
|
status: ParsingStatus
|
||||||
|
valueClass: ValueClass
|
||||||
|
typification: string
|
||||||
|
syntaxTree: string
|
||||||
|
args: IFunctionArg[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConstituentaID extends Pick<IConstituentaMeta, 'id'>{}
|
export interface IConstituentaID extends Pick<IConstituentaMeta, 'id'>{}
|
||||||
|
@ -431,35 +422,35 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
|
||||||
((cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0, 0),
|
((cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0, 0),
|
||||||
|
|
||||||
count_termin: result.items.reduce(
|
count_termin: result.items.reduce(
|
||||||
(sum, cst) => (sum + (cst.term?.raw ? 1 : 0) || 0), 0),
|
(sum, cst) => (sum + (cst.term_raw ? 1 : 0) || 0), 0),
|
||||||
count_definition: result.items.reduce(
|
count_definition: result.items.reduce(
|
||||||
(sum, cst) => (sum + (cst.definition?.text.raw ? 1 : 0) || 0), 0),
|
(sum, cst) => (sum + (cst.definition_raw ? 1 : 0) || 0), 0),
|
||||||
count_convention: result.items.reduce(
|
count_convention: result.items.reduce(
|
||||||
(sum, cst) => (sum + (cst.convention ? 1 : 0) || 0), 0),
|
(sum, cst) => (sum + (cst.convention ? 1 : 0) || 0), 0),
|
||||||
|
|
||||||
count_base: result.items.reduce(
|
count_base: result.items.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.BASE ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.BASE ? 1 : 0), 0),
|
||||||
count_constant: result.items?.reduce(
|
count_constant: result.items?.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.CONSTANT ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.CONSTANT ? 1 : 0), 0),
|
||||||
count_structured: result.items?.reduce(
|
count_structured: result.items?.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.STRUCTURED ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.STRUCTURED ? 1 : 0), 0),
|
||||||
count_axiom: result.items?.reduce(
|
count_axiom: result.items?.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.AXIOM ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.AXIOM ? 1 : 0), 0),
|
||||||
count_term: result.items.reduce(
|
count_term: result.items.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.TERM ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.TERM ? 1 : 0), 0),
|
||||||
count_function: result.items.reduce(
|
count_function: result.items.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.FUNCTION ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.FUNCTION ? 1 : 0), 0),
|
||||||
count_predicate: result.items.reduce(
|
count_predicate: result.items.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.PREDICATE ? 1 : 0), 0),
|
(sum, cst) => sum + (cst.cst_type === CstType.PREDICATE ? 1 : 0), 0),
|
||||||
count_theorem: result.items.reduce(
|
count_theorem: result.items.reduce(
|
||||||
(sum, cst) => sum + (cst.cstType === CstType.THEOREM ? 1 : 0), 0)
|
(sum, cst) => sum + (cst.cst_type === CstType.THEOREM ? 1 : 0), 0)
|
||||||
}
|
}
|
||||||
result.items.forEach(cst => {
|
result.items.forEach(cst => {
|
||||||
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
|
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
|
||||||
cst.isTemplate = inferTemplate(cst.definition.formal);
|
cst.is_template = inferTemplate(cst.definition_formal);
|
||||||
cst.cstClass = inferClass(cst.cstType, cst.isTemplate);
|
cst.cst_class = inferClass(cst.cst_type, cst.is_template);
|
||||||
result.graph.addNode(cst.id);
|
result.graph.addNode(cst.id);
|
||||||
const dependencies = extractGlobals(cst.definition.formal);
|
const dependencies = extractGlobals(cst.definition_formal);
|
||||||
dependencies.forEach(value => {
|
dependencies.forEach(value => {
|
||||||
const source = schema.items.find(cst => cst.alias === value)
|
const source = schema.items.find(cst => cst.alias === value)
|
||||||
if (source) {
|
if (source) {
|
||||||
|
@ -476,15 +467,15 @@ export function matchConstituenta(query: string, target: IConstituenta, mode: Cs
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TERM) &&
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TERM) &&
|
||||||
target.term.resolved.match(query)) {
|
target.term_resolved.match(query)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.EXPR) &&
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.EXPR) &&
|
||||||
target.definition.formal.match(query)) {
|
target.definition_formal.match(query)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TEXT)) {
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TEXT)) {
|
||||||
return (target.definition.text.resolved.match(query) || target.convention.match(query));
|
return (target.definition_resolved.match(query) || target.convention.match(query));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,18 @@ export interface IDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCstDescription(cst: IConstituenta): string {
|
export function getCstDescription(cst: IConstituenta): string {
|
||||||
if (cst.cstType === CstType.STRUCTURED) {
|
if (cst.cst_type === CstType.STRUCTURED) {
|
||||||
return (
|
return (
|
||||||
cst.term.resolved || cst.term.raw ||
|
cst.term_resolved || cst.term_raw ||
|
||||||
cst.definition.text.resolved || cst.definition.text.raw ||
|
cst.definition_resolved || cst.definition_raw ||
|
||||||
cst.convention ||
|
cst.convention ||
|
||||||
cst.definition.formal
|
cst.definition_formal
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
cst.term.resolved || cst.term.raw ||
|
cst.term_resolved || cst.term_raw ||
|
||||||
cst.definition.text.resolved || cst.definition.text.raw ||
|
cst.definition_resolved || cst.definition_raw ||
|
||||||
cst.definition.formal ||
|
cst.definition_formal ||
|
||||||
cst.convention
|
cst.convention
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export function getCstTypePrefix(type: CstType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCstExpressionPrefix(cst: IConstituenta): string {
|
export function getCstExpressionPrefix(cst: IConstituenta): string {
|
||||||
return cst.alias + (cst.cstType === CstType.STRUCTURED ? '::=' : ':==');
|
return cst.alias + (cst.cst_type === CstType.STRUCTURED ? '::=' : ':==');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRSButtonData(id: TokenID): IDescriptor {
|
export function getRSButtonData(id: TokenID): IDescriptor {
|
||||||
|
@ -424,7 +424,7 @@ export function createAliasFor(type: CstType, schema: IRSForm): string {
|
||||||
return `${prefix}1`;
|
return `${prefix}1`;
|
||||||
}
|
}
|
||||||
const index = schema.items.reduce((prev, cst, index) => {
|
const index = schema.items.reduce((prev, cst, index) => {
|
||||||
if (cst.cstType !== type) {
|
if (cst.cst_type !== type) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
index = Number(cst.alias.slice(1 - cst.alias.length)) + 1;
|
index = Number(cst.alias.slice(1 - cst.alias.length)) + 1;
|
||||||
|
@ -433,27 +433,23 @@ export function createAliasFor(type: CstType, schema: IRSForm): string {
|
||||||
return `${prefix}${index}`;
|
return `${prefix}${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMockConstituenta(id: number, alias: string, type: CstType, comment: string): IConstituenta {
|
export function getMockConstituenta(schema: number, id: number, alias: string, type: CstType, comment: string): IConstituenta {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
|
order: -1,
|
||||||
|
schema: schema,
|
||||||
alias: alias,
|
alias: alias,
|
||||||
convention: comment,
|
convention: comment,
|
||||||
cstType: type,
|
cst_type: type,
|
||||||
term: {
|
term_raw: '',
|
||||||
raw: '',
|
term_resolved: '',
|
||||||
resolved: '',
|
term_forms: [],
|
||||||
forms: []
|
definition_formal: '',
|
||||||
},
|
definition_raw: '',
|
||||||
definition: {
|
definition_resolved: '',
|
||||||
formal: '',
|
|
||||||
text: {
|
|
||||||
raw: '',
|
|
||||||
resolved: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: ExpressionStatus.INCORRECT,
|
status: ExpressionStatus.INCORRECT,
|
||||||
isTemplate: false,
|
is_template: false,
|
||||||
cstClass: CstClass.DERIVED,
|
cst_class: CstClass.DERIVED,
|
||||||
parse: {
|
parse: {
|
||||||
status: ParsingStatus.INCORRECT,
|
status: ParsingStatus.INCORRECT,
|
||||||
valueClass: ValueClass.INVALID,
|
valueClass: ValueClass.INVALID,
|
||||||
|
@ -628,6 +624,7 @@ export function getNodeLabel(node: ISyntaxTreeNode): string {
|
||||||
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARATION'
|
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARATION'
|
||||||
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARATION'
|
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARATION'
|
||||||
case TokenID.PUNC_DEFINE: return 'DEFINITION'
|
case TokenID.PUNC_DEFINE: return 'DEFINITION'
|
||||||
|
case TokenID.PUNC_STRUCT: return 'STRUCTURE_DEFITION'
|
||||||
|
|
||||||
case TokenID.NT_ARG_DECL: return 'ARG'
|
case TokenID.NT_ARG_DECL: return 'ARG'
|
||||||
case TokenID.NT_FUNC_CALL: return 'CALL'
|
case TokenID.NT_FUNC_CALL: return 'CALL'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
|
||||||
import { dependencies } from './package.json'
|
import { dependencies } from './package.json'
|
||||||
|
|
||||||
|
@ -14,10 +14,16 @@ function renderChunks(deps: Record<string, string>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default (({ mode }: { mode: string }) => {
|
||||||
|
process.env = {...process.env, ...loadEnv(mode, process.cwd())};
|
||||||
|
const enableHttps = process.env.VITE_PORTAL_FRONT_HTTPS === 'true';
|
||||||
|
return defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000
|
port: Number(process.env.VITE_PORTAL_FRONT_PORT),
|
||||||
|
|
||||||
|
// NOTE: https is not used for dev builds currently
|
||||||
|
https: enableHttps,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 4000, // KB
|
chunkSizeWarningLimit: 4000, // KB
|
||||||
|
@ -25,9 +31,11 @@ export default defineConfig({
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
|
// Load chunks for dependencies separately
|
||||||
...renderChunks(dependencies),
|
...renderChunks(dependencies),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|
55
scripts/dev/LocalDevSetup.ps1
Normal file
55
scripts/dev/LocalDevSetup.ps1
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# Create venv and install dependencies + imports
|
||||||
|
|
||||||
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\fronted"
|
||||||
|
$envPath = "$backend\venv"
|
||||||
|
$python = "$envPath\Scripts\python.exe"
|
||||||
|
|
||||||
|
function LocalDevelopmentSetup() {
|
||||||
|
FrontendSetup
|
||||||
|
BackendSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
function FrontendSetup() {
|
||||||
|
Set-Location $frontend
|
||||||
|
& npm install
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackendSetup() {
|
||||||
|
Set-Location $backend
|
||||||
|
|
||||||
|
ClearPrevious
|
||||||
|
CreateEnv
|
||||||
|
InstallPips
|
||||||
|
InstallImports
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClearPrevious() {
|
||||||
|
if (Test-Path -Path $envPath) {
|
||||||
|
Write-Host "Removing previous env: $envPath`n" -ForegroundColor DarkGreen
|
||||||
|
Remove-Item $envPath -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateEnv() {
|
||||||
|
Write-Host "Creating python env: $envPath`n" -ForegroundColor DarkGreen
|
||||||
|
& 'python' -m venv $envPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstallPips() {
|
||||||
|
& $python -m pip install --upgrade pip
|
||||||
|
& $python -m pip install -r requirements_dev.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstallImports() {
|
||||||
|
$wheel = Get-Childitem -Path import\*win*.whl -Name
|
||||||
|
if (-not $wheel) {
|
||||||
|
Write-Error 'Missing import wheel'
|
||||||
|
Exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installing wheel: $wheel`n" -ForegroundColor DarkGreen
|
||||||
|
& $python -m pip install -I import\$wheel
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDevelopmentSetup
|
27
scripts/dev/PopulateDevData.ps1
Normal file
27
scripts/dev/PopulateDevData.ps1
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Initialize database !
|
||||||
|
# FOR DEVELOPEMENT BUILDS ONLY!
|
||||||
|
$container= Read-Host -Prompt "Enter backend container name: "
|
||||||
|
|
||||||
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
|
||||||
|
function PopulateDevData() {
|
||||||
|
ImportInitialData
|
||||||
|
CreateAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportInitialData() {
|
||||||
|
docker exec `
|
||||||
|
-it $container `
|
||||||
|
python manage.py loaddata $backend\fixtures\InitialData.json
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateAdmin() {
|
||||||
|
docker exec `
|
||||||
|
-e DJANGO_SUPERUSER_USERNAME=admin `
|
||||||
|
-e DJANGO_SUPERUSER_PASSWORD=1234 `
|
||||||
|
-e DJANGO_SUPERUSER_EMAIL=admin@admin.com `
|
||||||
|
-it $container python manage.py createsuperuser --noinput
|
||||||
|
}
|
||||||
|
|
||||||
|
PopulateDevData
|
||||||
|
pause
|
23
scripts/dev/RunCoverage.ps1
Normal file
23
scripts/dev/RunCoverage.ps1
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Run coverage analysis
|
||||||
|
|
||||||
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
|
||||||
|
function RunLinters() {
|
||||||
|
BackendCoverage
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackendCoverage() {
|
||||||
|
Set-Location $backend
|
||||||
|
|
||||||
|
$coverageExec = "$backend\venv\Scripts\coverage.exe"
|
||||||
|
$djangoSrc = "$backend\manage.py"
|
||||||
|
$exclude = '*/venv/*,*/tests/*,*/migrations/*,*__init__.py,manage.py,apps.py,urls.py,settings.py'
|
||||||
|
|
||||||
|
& $coverageExec run --omit=$exclude $djangoSrc test
|
||||||
|
& $coverageExec report
|
||||||
|
& $coverageExec html
|
||||||
|
|
||||||
|
Start-Process "$backend\htmlcov\index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
RunLinters
|
17
scripts/dev/RunLint.ps1
Normal file
17
scripts/dev/RunLint.ps1
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Run coverage analysis
|
||||||
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
|
||||||
|
function RunLinters() {
|
||||||
|
BackendLint
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackendLint() {
|
||||||
|
$pylint = "$backend\venv\Scripts\pylint.exe"
|
||||||
|
$mypy = "$backend\venv\Scripts\mypy.exe"
|
||||||
|
|
||||||
|
Set-Location $backend
|
||||||
|
& $pylint cctext project apps
|
||||||
|
& $mypy cctext project apps
|
||||||
|
}
|
||||||
|
|
||||||
|
RunLinters
|
|
@ -1,22 +1,25 @@
|
||||||
# Run local server
|
# Run local server
|
||||||
Param(
|
Param(
|
||||||
[switch] $freshStart
|
[switch] $freshStart
|
||||||
)
|
)
|
||||||
|
|
||||||
$pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe"
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
$djangoSrc = "$PSScriptRoot\backend\manage.py"
|
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\frontend"
|
||||||
|
|
||||||
|
$pyExec = "$backend\venv\Scripts\python.exe"
|
||||||
|
$djangoSrc = "$backend\manage.py"
|
||||||
$initialData = "fixtures/InitialData.json"
|
$initialData = "fixtures/InitialData.json"
|
||||||
|
|
||||||
function RunServer() {
|
function RunServer() {
|
||||||
RunBackend
|
BackendRun
|
||||||
RunFrontend
|
FrontendRun
|
||||||
Start-Sleep -Seconds 1
|
Start-Sleep -Seconds 1
|
||||||
Start-Process "http://localhost:8000/"
|
Start-Process "http://localhost:8000/"
|
||||||
Start-Process "http://localhost:3000/"
|
Start-Process "http://localhost:3000/"
|
||||||
}
|
}
|
||||||
|
|
||||||
function RunBackend() {
|
function BackendRun() {
|
||||||
Set-Location $PSScriptRoot\backend
|
Set-Location $backend
|
||||||
if ($freshStart) {
|
if ($freshStart) {
|
||||||
FlushData
|
FlushData
|
||||||
DoMigrations
|
DoMigrations
|
||||||
|
@ -30,15 +33,15 @@ function RunBackend() {
|
||||||
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'django'; & $pyExec $djangoSrc runserver }"
|
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'django'; & $pyExec $djangoSrc runserver }"
|
||||||
}
|
}
|
||||||
|
|
||||||
function RunFrontend() {
|
function FrontendRun() {
|
||||||
Set-Location $PSScriptRoot\frontend
|
Set-Location $frontend
|
||||||
& npm install
|
& npm install
|
||||||
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'react'; & npm run dev }"
|
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'react'; & npm run dev }"
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlushData {
|
function FlushData {
|
||||||
& $pyExec $djangoSrc flush --noinput
|
& $pyExec $djangoSrc flush --noinput
|
||||||
$dbPath = "$PSScriptRoot\backend\db.sqlite3"
|
$dbPath = "$backend\db.sqlite3"
|
||||||
if (Test-Path -Path $dbPath -PathType Leaf) {
|
if (Test-Path -Path $dbPath -PathType Leaf) {
|
||||||
Remove-Item $dbPath
|
Remove-Item $dbPath
|
||||||
}
|
}
|
25
scripts/dev/RunTests.ps1
Normal file
25
scripts/dev/RunTests.ps1
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Run tests
|
||||||
|
|
||||||
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\frontend"
|
||||||
|
|
||||||
|
function RunTests() {
|
||||||
|
TestBackend
|
||||||
|
TestFrontend
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestBackend() {
|
||||||
|
$pyExec = "$backend\venv\Scripts\python.exe"
|
||||||
|
$djangoSrc = "$backend\manage.py"
|
||||||
|
|
||||||
|
Set-Location $backend
|
||||||
|
& $pyExec $djangoSrc check
|
||||||
|
& $pyExec $djangoSrc test
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestFrontend() {
|
||||||
|
Set-Location $frontend
|
||||||
|
& npm test
|
||||||
|
}
|
||||||
|
|
||||||
|
RunTests
|
70
scripts/prod/CreateBackup.ps1
Normal file
70
scripts/prod/CreateBackup.ps1
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# ====== Create database backup ==========
|
||||||
|
# WARNING! DO NOT RUN THIS FILE AUTOMATICALLY FROM REPOSITORY LOCATION!
|
||||||
|
# Create a copy in secure location @production host. Update backup scripts from repository manually
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Input params
|
||||||
|
$backupLocation = "D:\DEV\backup\portal"
|
||||||
|
|
||||||
|
$containerDB = "dev-portal-db"
|
||||||
|
$containerBackend = "dev-portal-backend"
|
||||||
|
$pgUser = "portal-admin"
|
||||||
|
$pgDB = "portal-db"
|
||||||
|
|
||||||
|
# Internal params
|
||||||
|
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
|
||||||
|
$_date = Get-Date
|
||||||
|
$_formatDate = $_date.ToString("yyyy-MM-dd")
|
||||||
|
|
||||||
|
$destination = "{0}\{1}" -f $backupLocation, $_formatDate
|
||||||
|
|
||||||
|
function CreateBackup() {
|
||||||
|
EnsureLocationIsReady
|
||||||
|
PostgreDump
|
||||||
|
DjangoDump
|
||||||
|
Write-Host "Backup saved to $destination" -ForegroundColor DarkGreen
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnsureLocationIsReady() {
|
||||||
|
if (Test-Path -Path $destination) {
|
||||||
|
Write-Host "Removing previous unfinished backup: $destination`n" -ForegroundColor DarkRed
|
||||||
|
Remove-Item $destination -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $destination | Out-Null
|
||||||
|
if (Test-Path -Path $archive -PathType Leaf) {
|
||||||
|
Write-Host "Removing previous backup: $archive`n" -ForegroundColor DarkRed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostgreDump() {
|
||||||
|
$host_dbDump = "$destination\$_formatDate-db.dump"
|
||||||
|
$local_dbDump = "/home/$_formatDate-db.dump"
|
||||||
|
& docker exec $containerDB `
|
||||||
|
pg_dump `
|
||||||
|
--username=$pgUser `
|
||||||
|
--exclude-table=django_migrations `
|
||||||
|
--format=custom `
|
||||||
|
--dbname=$pgDB `
|
||||||
|
--file=$local_dbDump
|
||||||
|
& docker cp ${containerDB}:${local_dbDump} $host_dbDump
|
||||||
|
& docker exec $containerDB rm $local_dbDump
|
||||||
|
}
|
||||||
|
|
||||||
|
function DjangoDump() {
|
||||||
|
$host_dataDump = "$destination\$_formatDate-data.json.gz"
|
||||||
|
$local_dataDump = "/home/app/web/backup/$_formatDate-data.json"
|
||||||
|
$local_archiveDump = "/home/app/web/backup/$_formatDate-data.json.gz"
|
||||||
|
& docker exec $containerBackend `
|
||||||
|
python manage.py dumpdata `
|
||||||
|
--indent=2 `
|
||||||
|
--exclude=admin.LogEntry `
|
||||||
|
--exclude=sessions `
|
||||||
|
--exclude=contenttypes `
|
||||||
|
--exclude=auth.permission `
|
||||||
|
--output=$local_dataDump
|
||||||
|
& docker exec $containerBackend gzip --force $local_dataDump
|
||||||
|
& docker cp ${containerBackend}:${local_archiveDump} $host_dataDump
|
||||||
|
& docker exec $containerBackend rm $local_archiveDump
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateBackup
|
49
scripts/prod/CreateBackup.sh
Normal file
49
scripts/prod/CreateBackup.sh
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# ====== Create database backup ==========
|
||||||
|
# WARNING! DO NOT RUN THIS FILE AUTOMATICALLY FROM REPOSITORY LOCATION!
|
||||||
|
# Create a copy in secure location @production host. Update backup scripts from repository manually
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
backupLocation="/home/admuser/backup"
|
||||||
|
pgUser="portal-admin"
|
||||||
|
pgDB="portal-db"
|
||||||
|
containerDB="portal-db"
|
||||||
|
containerBackend="portal-backend"
|
||||||
|
|
||||||
|
dateFmt=$(date '+%Y-%m-%d')
|
||||||
|
destination="$backupLocation/$dateFmt"
|
||||||
|
|
||||||
|
EnsureLocation()
|
||||||
|
{
|
||||||
|
rm -rf $destination
|
||||||
|
mkdir $destination
|
||||||
|
}
|
||||||
|
|
||||||
|
PostgreDump()
|
||||||
|
{
|
||||||
|
dbDump="$destination/$dateFmt-db.dump"
|
||||||
|
docker exec $containerDB pg_dump \
|
||||||
|
--username=$pgUser \
|
||||||
|
--exclude-table=django_migrations \
|
||||||
|
--format=custom \
|
||||||
|
--dbname=$pgDB \
|
||||||
|
> $dbDump
|
||||||
|
}
|
||||||
|
|
||||||
|
DjangoDump()
|
||||||
|
{
|
||||||
|
dataDump="$destination/$dateFmt-data.json"
|
||||||
|
docker exec $containerBackend \
|
||||||
|
python manage.py dumpdata \
|
||||||
|
--indent=2 \
|
||||||
|
--exclude=admin.LogEntry \
|
||||||
|
--exclude=sessions \
|
||||||
|
--exclude=contenttypes \
|
||||||
|
--exclude=auth.permission \
|
||||||
|
> $dataDump
|
||||||
|
gzip --force $dataDump
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureLocation
|
||||||
|
PostgreDump
|
||||||
|
DjangoDump
|
||||||
|
echo "Backup created at: $destination"
|
19
scripts/prod/LoadDjangoBackup.ps1
Normal file
19
scripts/prod/LoadDjangoBackup.ps1
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# ====== Load database backup from Django dumpdata ==========
|
||||||
|
# WARNING! DO NOT RUN THIS FILE AUTOMATICALLY FROM REPOSITORY LOCATION!
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Input params
|
||||||
|
$dataArchive = "D:\DEV\backup\portal\2023-09-01\2023-09-01-data.json.gz"
|
||||||
|
$target = "local-portal-backend"
|
||||||
|
|
||||||
|
function LoadDjangoBackup() {
|
||||||
|
$local_archiveDump = "/home/app/web/backup/db-restore.json.gz"
|
||||||
|
$local_dataDump = "/home/app/web/backup/db-restore.json"
|
||||||
|
& docker cp ${dataArchive} ${target}:$local_archiveDump
|
||||||
|
& docker exec $target gzip --decompress --force $local_dataDump
|
||||||
|
docker exec $target `
|
||||||
|
python manage.py loaddata $local_dataDump
|
||||||
|
& docker exec $target rm $local_dataDump
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadDjangoBackup
|
23
scripts/prod/LoadPostgreBackup.ps1
Normal file
23
scripts/prod/LoadPostgreBackup.ps1
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# ====== Load database backup from PostgreSQL dump ==========
|
||||||
|
# WARNING! DO NOT RUN THIS FILE AUTOMATICALLY FROM REPOSITORY LOCATION!
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Input params
|
||||||
|
$dataDump = "D:\DEV\backup\portal\2023-09-01\2023-09-01-db.dump"
|
||||||
|
$target = "dev-portal-db"
|
||||||
|
$pgUser = "portal-admin"
|
||||||
|
$pgDB = "portal-db"
|
||||||
|
|
||||||
|
function LoadPostgreBackup() {
|
||||||
|
$local_dbDump = "/home/db-restore.dump"
|
||||||
|
& docker cp ${dataDump} ${target}:$local_dbDump
|
||||||
|
docker exec $target `
|
||||||
|
pg_restore `
|
||||||
|
--username=$pgUser `
|
||||||
|
--dbname=$pgDB `
|
||||||
|
--clean `
|
||||||
|
$local_dbDump
|
||||||
|
& docker exec $target rm $local_dbDump
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadPostgreBackup
|
Loading…
Reference in New Issue
Block a user