mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
Initial commit
This commit is contained in:
commit
c7549b1e07
62
.dockerignore
Normal file
62
.dockerignore
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
|
||||
# Windows specific
|
||||
*.ps1
|
||||
|
||||
# Environment variables
|
||||
.env.*
|
||||
*/.env.*
|
||||
|
||||
|
||||
# Local build/utility folders
|
||||
**/venv
|
||||
**/build
|
||||
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
|
||||
# Django
|
||||
rsconcept/frontend/static
|
||||
rsconcept/frontend/media
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# React
|
||||
.DS_*
|
||||
*.log
|
||||
logs
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
*.sublime*
|
||||
|
||||
|
||||
# Specific items
|
||||
docker-compose.yml
|
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
# SECURITY SENSITIVE FILES
|
||||
# persistent/*
|
||||
|
||||
# External distributions
|
||||
rsconcept/backend/import/*.whl
|
||||
rsconcept/backend/static
|
||||
rsconcept/backend/media
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
|
||||
# Django
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
|
||||
# React
|
||||
.DS_*
|
||||
*.log
|
||||
logs
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
*.sublime*
|
||||
|
||||
|
||||
# Environments
|
||||
venv/
|
57
.vscode/launch.json
vendored
Normal file
57
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "Test",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/rsconcept/RunTests.ps1",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "BE-Coverage",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/rsconcept/RunCoverage.ps1",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "BE-DebugTest",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/rsconcept/backend",
|
||||
"program": "${workspaceFolder}/rsconcept/backend/manage.py",
|
||||
"args": [
|
||||
"test"
|
||||
],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
"name": "BE-Debug",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/rsconcept/backend/manage.py",
|
||||
"args": [
|
||||
"runserver"
|
||||
],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
"name": "Restart",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
||||
"args": ["-freshStart"]
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.enabled": true
|
||||
}
|
7
TODO.txt
Normal file
7
TODO.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
!! This is not complete list of todos !!
|
||||
This list only contains global tech refactorings and tech debt
|
||||
For more specific TODOs see comments in code
|
||||
|
||||
- Use migtation/fixtures to provide initial data for testing
|
||||
- USe migtation/fixtures to load example common data
|
||||
- Add HTTPS for deployment
|
62
docker-compose.yml
Normal file
62
docker-compose.yml
Normal file
|
@ -0,0 +1,62 @@
|
|||
version: "3.9"
|
||||
|
||||
volumes:
|
||||
postgres_volume:
|
||||
name: "postgres-db"
|
||||
django_static_volume:
|
||||
name: "static"
|
||||
django_media_volume:
|
||||
name: "media"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: concept-api-net
|
||||
|
||||
services:
|
||||
frontend:
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
build:
|
||||
context: ./rsconcept/frontend
|
||||
ports:
|
||||
- 3000:3000
|
||||
command: serve -s /home/node -l 3000
|
||||
|
||||
|
||||
backend:
|
||||
restart: always
|
||||
depends_on:
|
||||
- postgresql-db
|
||||
- nginx
|
||||
build:
|
||||
context: ./rsconcept/backend
|
||||
env_file: ./rsconcept/backend/.env.dev
|
||||
ports:
|
||||
- 8000:8000
|
||||
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:8000
|
||||
|
||||
|
||||
postgresql-db:
|
||||
restart: always
|
||||
image: postgres:alpine
|
||||
env_file: ./postgresql/.env.dev
|
||||
volumes:
|
||||
- postgres_volume:/var/lib/postgresql/data
|
||||
|
||||
|
||||
nginx:
|
||||
restart: always
|
||||
build:
|
||||
context: ./nginx
|
||||
ports:
|
||||
- 1337:80
|
||||
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
|
||||
|
4
nginx/Dockerfile
Normal file
4
nginx/Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
FROM nginx:stable-alpine3.17-slim
|
||||
|
||||
# Сopу nginx configuration to the proxy-server
|
||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
22
nginx/default.conf
Normal file
22
nginx/default.conf
Normal file
|
@ -0,0 +1,22 @@
|
|||
upstream innerdjango {
|
||||
server backend:8000;
|
||||
# `backend` is the service's name in docker-compose.yml,
|
||||
# The `innerdjango` is the name of upstream, used by nginx below.
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name rs.acconcept.ru;
|
||||
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/;
|
||||
}
|
||||
}
|
3
postgresql/.env.dev
Normal file
3
postgresql/.env.dev
Normal file
|
@ -0,0 +1,3 @@
|
|||
POSTGRES_USER=dev-test-user
|
||||
POSTGRES_PASSWORD=02BD82EE0D
|
||||
POSTGRES_DB=dev-db
|
12
rsconcept/RunCoverage.ps1
Normal file
12
rsconcept/RunCoverage.ps1
Normal file
|
@ -0,0 +1,12 @@
|
|||
# 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:///D:/DEV/!WORK/Concept-Web/rsconcept/backend/htmlcov/index.html"
|
60
rsconcept/RunServer.ps1
Normal file
60
rsconcept/RunServer.ps1
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Run local server
|
||||
Param(
|
||||
[switch] $freshStart
|
||||
)
|
||||
|
||||
$pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe"
|
||||
$djangoSrc = "$PSScriptRoot\backend\manage.py"
|
||||
|
||||
function RunServer() {
|
||||
RunBackend
|
||||
RunFrontend
|
||||
Start-Sleep -Seconds 1
|
||||
Start-Process "http://127.0.0.1:8000/"
|
||||
}
|
||||
|
||||
function RunBackend() {
|
||||
Set-Location $PSScriptRoot\backend
|
||||
if ($freshStart) {
|
||||
FlushData
|
||||
DoMigrations
|
||||
PrepareStatic -clearPrevious
|
||||
AddAdmin
|
||||
} else {
|
||||
DoMigrations
|
||||
PrepareStatic
|
||||
}
|
||||
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'django'; & $pyExec $djangoSrc runserver }"
|
||||
}
|
||||
|
||||
function RunFrontend() {
|
||||
Set-Location $PSScriptRoot\frontend
|
||||
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'react'; & npm run start }"
|
||||
}
|
||||
|
||||
function FlushData {
|
||||
& $pyExec $djangoSrc flush --no-input
|
||||
Remove-Item $PSScriptRoot\backend\db.sqlite3
|
||||
}
|
||||
|
||||
function AddAdmin {
|
||||
$env:DJANGO_SUPERUSER_USERNAME = 'admin'
|
||||
$env:DJANGO_SUPERUSER_PASSWORD = '1234'
|
||||
$env:DJANGO_SUPERUSER_EMAIL = 'admin@admin.com'
|
||||
& $pyExec $djangoSrc createsuperuser --noinput
|
||||
}
|
||||
|
||||
function DoMigrations {
|
||||
& $pyExec $djangoSrc makemigrations
|
||||
& $pyExec $djangoSrc migrate
|
||||
}
|
||||
|
||||
function PrepareStatic([switch]$clearPrevious) {
|
||||
if ($clearPrevious) {
|
||||
& $pyExec $djangoSrc collectstatic --noinput --clear
|
||||
} else {
|
||||
& $pyExec $djangoSrc collectstatic --noinput
|
||||
}
|
||||
}
|
||||
|
||||
RunServer
|
8
rsconcept/RunTests.ps1
Normal file
8
rsconcept/RunTests.ps1
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Run tests
|
||||
Set-Location $PSScriptRoot\backend
|
||||
|
||||
$pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe"
|
||||
$djangoSrc = "$PSScriptRoot\backend\manage.py"
|
||||
|
||||
& $pyExec $djangoSrc check
|
||||
& $pyExec $djangoSrc test
|
1
rsconcept/backend/!Readme.txt
Normal file
1
rsconcept/backend/!Readme.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Импортируемые предкомпилированные пакеты (*.whl) следует класть в import\
|
36
rsconcept/backend/.dockerignore
Normal file
36
rsconcept/backend/.dockerignore
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Windows specific
|
||||
*.ps1
|
||||
|
||||
# Dev specific
|
||||
requirements_dev.txt
|
||||
.gitignore
|
||||
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
|
||||
# Django
|
||||
static/
|
||||
media/
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
23
rsconcept/backend/.env.dev
Normal file
23
rsconcept/backend/.env.dev
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Application settings
|
||||
SECRET_KEY=django-insecure-)rq@!&v7l2r%2%q#n!uq+zk@=&yc0^&ql^7%2!%9u)vt1x&j=d
|
||||
ALLOWED_HOSTS=rs.acconcept.ru;localhost
|
||||
|
||||
|
||||
# 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=dev-db
|
||||
DB_USER=dev-test-user
|
||||
DB_PASSWORD=02BD82EE0D
|
||||
DB_HOST=postgresql-db
|
||||
DB_PORT=5432
|
||||
|
||||
|
||||
# Debug settings
|
||||
DEBUG=1
|
||||
PYTHONDEVMODE=1
|
||||
PYTHONTRACEMALLOC=1
|
70
rsconcept/backend/Dockerfile
Normal file
70
rsconcept/backend/Dockerfile
Normal file
|
@ -0,0 +1,70 @@
|
|||
# ==========================================
|
||||
# ============ Multi-stage build ===========
|
||||
# ==========================================
|
||||
FROM ubuntu:jammy as python-base
|
||||
|
||||
RUN apt-get update -qq && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python-is-python3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install wheel
|
||||
|
||||
# ========= Builder ==============
|
||||
FROM python-base as builder
|
||||
|
||||
# Set env variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
COPY ./requirements.txt ./
|
||||
COPY ./import/*linux*.whl ./wheels/
|
||||
RUN pip wheel \
|
||||
--no-cache-dir --no-deps \
|
||||
--wheel-dir=/wheels -r requirements.txt
|
||||
|
||||
|
||||
# ======== Application ============
|
||||
FROM python-base
|
||||
|
||||
# Install security updates and system packages
|
||||
RUN apt-get update -qq && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y \
|
||||
netcat && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup the app user
|
||||
ENV USER_HOME=/home/app
|
||||
ENV APP_HOME=/home/app/web
|
||||
|
||||
RUN mkdir -p $USER_HOME && \
|
||||
mkdir -p $APP_HOME && \
|
||||
mkdir -p $APP_HOME/static && \
|
||||
mkdir -p $APP_HOME/media && \
|
||||
adduser --system --group app
|
||||
|
||||
# Install python dependencies
|
||||
WORKDIR $APP_HOME
|
||||
COPY --from=builder /wheels /wheels
|
||||
RUN pip install --no-cache /wheels/* && \
|
||||
rm -rf /wheels
|
||||
|
||||
# Copy application sources and setup permissions
|
||||
COPY apps/ ./apps/
|
||||
COPY project/ ./project
|
||||
COPY manage.py entrypoint.sh ./
|
||||
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
|
||||
chmod +x $APP_HOME/entrypoint.sh && \
|
||||
chown -R app:app $APP_HOME && \
|
||||
chmod -R a+rwx $APP_HOME/static && \
|
||||
chmod -R a+rwx $APP_HOME/media
|
||||
|
||||
USER app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
ENTRYPOINT ["sh", "entrypoint.sh"]
|
24
rsconcept/backend/LocalEnvSetup.ps1
Normal file
24
rsconcept/backend/LocalEnvSetup.ps1
Normal file
|
@ -0,0 +1,24 @@
|
|||
# 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
|
0
rsconcept/backend/apps/__init__.py
Normal file
0
rsconcept/backend/apps/__init__.py
Normal file
0
rsconcept/backend/apps/rsform/__init__.py
Normal file
0
rsconcept/backend/apps/rsform/__init__.py
Normal file
15
rsconcept/backend/apps/rsform/admin.py
Normal file
15
rsconcept/backend/apps/rsform/admin.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class ConstituentaAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class RSFormAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(models.Constituenta, ConstituentaAdmin)
|
||||
admin.site.register(models.RSForm, RSFormAdmin)
|
6
rsconcept/backend/apps/rsform/apps.py
Normal file
6
rsconcept/backend/apps/rsform/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RsformConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.rsform'
|
55
rsconcept/backend/apps/rsform/migrations/0001_initial.py
Normal file
55
rsconcept/backend/apps/rsform/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Generated by Django 4.2.1 on 2023-05-18 18:00
|
||||
|
||||
import apps.rsform.models
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RSForm',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.TextField(verbose_name='Название')),
|
||||
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Комментарий')),
|
||||
('is_common', models.BooleanField(default=False, verbose_name='Общая')),
|
||||
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Схема',
|
||||
'verbose_name_plural': 'Схемы',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Constituenta',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')),
|
||||
('alias', models.CharField(max_length=8, verbose_name='Имя')),
|
||||
('csttype', models.CharField(choices=[('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип')),
|
||||
('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')),
|
||||
('term', models.JSONField(default=apps.rsform.models._empty_term, verbose_name='Термин')),
|
||||
('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')),
|
||||
('definition_text', models.JSONField(blank=True, default=apps.rsform.models._empty_definition, verbose_name='Текстовое определние')),
|
||||
('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Конституета',
|
||||
'verbose_name_plural': 'Конституенты',
|
||||
'unique_together': {('schema', 'alias'), ('schema', 'order')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
import os
|
||||
from django.db import migrations
|
||||
|
||||
from apps.rsform import utils
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
def load_initial_schemas(apps, schema_editor):
|
||||
rootdir = os.path.join(os.getcwd(), 'data')
|
||||
for subdir, dirs, files in os.walk(rootdir):
|
||||
for file in files:
|
||||
data = utils.read_trs(os.path.join(subdir, file))
|
||||
RSForm.import_json(None, data)
|
||||
|
||||
|
||||
def load_initial_users(apps, schema_editor):
|
||||
for n in range(1, 10, 1):
|
||||
User.objects.create_user(
|
||||
f'TestUser{n}', f'usermail{n}@gmail.com', '1234'
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('rsform', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(load_initial_schemas),
|
||||
migrations.RunPython(load_initial_users),
|
||||
]
|
214
rsconcept/backend/apps/rsform/models.py
Normal file
214
rsconcept/backend/apps/rsform/models.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
from django.db import models, transaction
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class CstType(models.TextChoices):
|
||||
''' Type of constituenta '''
|
||||
BASE = 'basic'
|
||||
CONSTANT = 'constant'
|
||||
STRUCTURED = 'structure'
|
||||
AXIOM = 'axiom'
|
||||
TERM = 'term'
|
||||
FUNCTION = 'function'
|
||||
PREDICATE = 'predicate'
|
||||
THEOREM = 'theorem'
|
||||
|
||||
|
||||
class Syntax(models.TextChoices):
|
||||
''' Syntax types '''
|
||||
UNDEF = 'undefined'
|
||||
ASCII = 'ascii'
|
||||
MATH = 'math'
|
||||
|
||||
|
||||
def _empty_term():
|
||||
return {'raw': '', 'resolved': '', 'forms': []}
|
||||
|
||||
|
||||
def _empty_definition():
|
||||
return {'raw': '', 'resolved': ''}
|
||||
|
||||
|
||||
class RSForm(models.Model):
|
||||
''' RSForm is a math form of capturing conceptual schema '''
|
||||
owner = models.ForeignKey(
|
||||
verbose_name='Владелец',
|
||||
to=User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True
|
||||
)
|
||||
title = models.TextField(
|
||||
verbose_name='Название'
|
||||
)
|
||||
alias = models.CharField(
|
||||
verbose_name='Шифр',
|
||||
max_length=255,
|
||||
blank=True
|
||||
)
|
||||
comment = models.TextField(
|
||||
verbose_name='Комментарий',
|
||||
blank=True
|
||||
)
|
||||
is_common = models.BooleanField(
|
||||
verbose_name='Общая',
|
||||
default=False
|
||||
)
|
||||
time_create = models.DateTimeField(
|
||||
verbose_name='Дата создания',
|
||||
auto_now_add=True
|
||||
)
|
||||
time_update = models.DateTimeField(
|
||||
verbose_name='Дата изменения',
|
||||
auto_now=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Схема'
|
||||
verbose_name_plural = 'Схемы'
|
||||
|
||||
def constituents(self) -> models.QuerySet:
|
||||
''' Get QuerySet containing all constituents of current RSForm '''
|
||||
return Constituenta.objects.filter(schema=self)
|
||||
|
||||
@transaction.atomic
|
||||
def insert_at(self, position: int, alias: str, type: CstType) -> 'Constituenta':
|
||||
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
|
||||
if position <= 0:
|
||||
raise ValidationError('Invalid position: should be positive integer')
|
||||
update_list = Constituenta.objects.filter(schema=self, order__gte=position)
|
||||
for cst in update_list:
|
||||
cst.order += 1
|
||||
cst.save()
|
||||
return Constituenta.objects.create(
|
||||
schema=self,
|
||||
order=position,
|
||||
alias=alias,
|
||||
csttype=type
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def insert_last(self, alias: str, type: CstType) -> 'Constituenta':
|
||||
''' Insert new constituenta at last position '''
|
||||
position = 1
|
||||
if self.constituents().exists():
|
||||
position += self.constituents().aggregate(models.Max('order'))['order__max']
|
||||
return Constituenta.objects.create(
|
||||
schema=self,
|
||||
order=position,
|
||||
alias=alias,
|
||||
csttype=type
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
||||
schema = RSForm.objects.create(
|
||||
title=data.get('title', 'Без названия'),
|
||||
owner=owner,
|
||||
alias=data.get('alias', ''),
|
||||
comment=data.get('comment', ''),
|
||||
is_common=is_common
|
||||
)
|
||||
order = 1
|
||||
for cst in data['items']:
|
||||
# TODO: get rid of empty_term etc. Use None instead
|
||||
Constituenta.objects.create(
|
||||
alias=cst['alias'],
|
||||
schema=schema,
|
||||
order=order,
|
||||
csttype=cst['cstType'],
|
||||
convention=cst.get('convention', 'Без названия'),
|
||||
definition_formal=cst['definition'].get('formal', '') if 'definition' in cst else '',
|
||||
term=cst.get('term', _empty_term()),
|
||||
definition_text=cst['definition']['text'] \
|
||||
if 'definition' in cst and 'text' in cst['definition'] else _empty_definition() # noqa: E502
|
||||
)
|
||||
order += 1
|
||||
return schema
|
||||
|
||||
def to_json(self) -> str:
|
||||
''' Generate JSON string containing all data from RSForm '''
|
||||
result = self._prepare_json_rsform()
|
||||
items = self.constituents().order_by('order')
|
||||
for cst in items:
|
||||
result['items'].append(self._prepare_json_cst(cst))
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def _prepare_json_rsform(self: 'Constituenta') -> dict:
|
||||
return {
|
||||
'type': 'rsform',
|
||||
'title': self.title,
|
||||
'alias': self.alias,
|
||||
'comment': self.comment,
|
||||
'items': []
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _prepare_json_cst(cst: 'Constituenta') -> dict:
|
||||
return {
|
||||
'entityUID': cst.id,
|
||||
'type': 'constituenta',
|
||||
'cstType': cst.csttype,
|
||||
'alias': cst.alias,
|
||||
'convention': cst.convention,
|
||||
'term': cst.term,
|
||||
'definition': {
|
||||
'formal': cst.definition_formal,
|
||||
'text': cst.definition_text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Constituenta(models.Model):
|
||||
''' Constituenta is the base unit for every conceptual schema '''
|
||||
schema = models.ForeignKey(
|
||||
verbose_name='Концептуальная схема',
|
||||
to=RSForm,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
order = models.PositiveIntegerField(
|
||||
verbose_name='Позиция',
|
||||
validators=[MinValueValidator(1)]
|
||||
)
|
||||
alias = models.CharField(
|
||||
verbose_name='Имя',
|
||||
max_length=8
|
||||
)
|
||||
csttype = models.CharField(
|
||||
verbose_name='Тип',
|
||||
max_length=10,
|
||||
choices=CstType.choices,
|
||||
default=CstType.BASE
|
||||
)
|
||||
convention = models.TextField(
|
||||
verbose_name='Комментарий/Конвенция',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
term = models.JSONField(
|
||||
verbose_name='Термин',
|
||||
default=_empty_term
|
||||
)
|
||||
definition_formal = models.TextField(
|
||||
verbose_name='Родоструктурное определение',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
definition_text = models.JSONField(
|
||||
verbose_name='Текстовое определние',
|
||||
default=_empty_definition,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Конституета'
|
||||
verbose_name_plural = 'Конституенты'
|
||||
unique_together = (('schema', 'alias'), ('schema', 'order'))
|
||||
|
||||
def __str__(self):
|
||||
return self.alias
|
18
rsconcept/backend/apps/rsform/serializers.py
Normal file
18
rsconcept/backend/apps/rsform/serializers.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import RSForm
|
||||
|
||||
|
||||
class FileSerializer(serializers.Serializer):
|
||||
file = serializers.FileField(allow_empty_file=False)
|
||||
|
||||
|
||||
class ExpressionSerializer(serializers.Serializer):
|
||||
expression = serializers.CharField()
|
||||
|
||||
|
||||
class RSFormSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RSForm
|
||||
fields = '__all__'
|
||||
read_only_fields = ('owner', 'id')
|
5
rsconcept/backend/apps/rsform/tests/__init__.py
Normal file
5
rsconcept/backend/apps/rsform/tests/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
# flake8: noqa
|
||||
from .t_imports import *
|
||||
from .t_views import *
|
||||
from .t_models import *
|
||||
from .t_serializers import *
|
BIN
rsconcept/backend/apps/rsform/tests/data/sample-rsform.trs
Normal file
BIN
rsconcept/backend/apps/rsform/tests/data/sample-rsform.trs
Normal file
Binary file not shown.
132
rsconcept/backend/apps/rsform/tests/t_imports.py
Normal file
132
rsconcept/backend/apps/rsform/tests/t_imports.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
''' Testing imported pyconcept functionality '''
|
||||
from unittest import TestCase
|
||||
|
||||
import pyconcept as pc
|
||||
import json
|
||||
|
||||
|
||||
class TestIntegrations(TestCase):
|
||||
def test_convert_to_ascii(self):
|
||||
''' Test converting to ASCII syntax '''
|
||||
self.assertEqual(pc.convert_to_ascii(''), '')
|
||||
self.assertEqual(pc.convert_to_ascii('\u212c(X1)'), r'B(X1)')
|
||||
|
||||
def test_convert_to_math(self):
|
||||
''' Test converting to MATH syntax '''
|
||||
self.assertEqual(pc.convert_to_math(''), '')
|
||||
self.assertEqual(pc.convert_to_math(r'B(X1)'), '\u212c(X1)')
|
||||
|
||||
def test_parse_expression(self):
|
||||
''' Test parsing expression '''
|
||||
out = json.loads(pc.parse_expression('X1=X2'))
|
||||
self.assertEqual(out['parseResult'], True)
|
||||
self.assertEqual(out['syntax'], 'math')
|
||||
|
||||
def test_empty_schema(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
pc.check_schema('')
|
||||
|
||||
def test_check_schema(self):
|
||||
schema = self._default_schema()
|
||||
self.assertTrue(pc.check_schema(schema) != '')
|
||||
|
||||
def test_check_expression(self):
|
||||
schema = self._default_schema()
|
||||
out1 = json.loads(pc.check_expression(schema, 'X1=X1'))
|
||||
self.assertTrue(out1['parseResult'])
|
||||
|
||||
out2 = json.loads(pc.check_expression(schema, 'X1=X2'))
|
||||
self.assertFalse(out2['parseResult'])
|
||||
|
||||
def test_reset_aliases(self):
|
||||
''' Test reset aliases in schema '''
|
||||
schema = self._default_schema()
|
||||
fixedSchema = json.loads(pc.reset_aliases(schema))
|
||||
self.assertTrue(len(fixedSchema['items']) > 2)
|
||||
self.assertEqual(fixedSchema['items'][2]['alias'], 'S1')
|
||||
|
||||
def _default_schema(self):
|
||||
return '''{
|
||||
"type": "rsform",
|
||||
"title": "default",
|
||||
"alias": "default",
|
||||
"comment": "",
|
||||
"items": [
|
||||
{
|
||||
"entityUID": 1023383816,
|
||||
"type": "constituenta",
|
||||
"cstType": "basic",
|
||||
"alias": "X1",
|
||||
"convention": "",
|
||||
"term": {
|
||||
"raw": "",
|
||||
"resolved": "",
|
||||
"forms": []
|
||||
},
|
||||
"definition": {
|
||||
"formal": "",
|
||||
"text": {
|
||||
"raw": "",
|
||||
"resolved": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entityUID": 1877659352,
|
||||
"type": "constituenta",
|
||||
"cstType": "basic",
|
||||
"alias": "X2",
|
||||
"convention": "",
|
||||
"term": {
|
||||
"raw": "",
|
||||
"resolved": "",
|
||||
"forms": []
|
||||
},
|
||||
"definition": {
|
||||
"formal": "",
|
||||
"text": {
|
||||
"raw": "",
|
||||
"resolved": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entityUID": 1115937389,
|
||||
"type": "constituenta",
|
||||
"cstType": "structure",
|
||||
"alias": "S2",
|
||||
"convention": "",
|
||||
"term": {
|
||||
"raw": "",
|
||||
"resolved": "",
|
||||
"forms": []
|
||||
},
|
||||
"definition": {
|
||||
"formal": "ℬ(X1×X1)",
|
||||
"text": {
|
||||
"raw": "",
|
||||
"resolved": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entityUID": 94433573,
|
||||
"type": "constituenta",
|
||||
"cstType": "structure",
|
||||
"alias": "S3",
|
||||
"convention": "",
|
||||
"term": {
|
||||
"raw": "",
|
||||
"resolved": "",
|
||||
"forms": []
|
||||
},
|
||||
"definition": {
|
||||
"formal": "ℬ(X1×X2)",
|
||||
"text": {
|
||||
"raw": "",
|
||||
"resolved": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}'''
|
225
rsconcept/backend/apps/rsform/tests/t_models.py
Normal file
225
rsconcept/backend/apps/rsform/tests/t_models.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
''' Testing models '''
|
||||
import json
|
||||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from django.forms import ValidationError
|
||||
|
||||
from apps.rsform.models import (
|
||||
RSForm,
|
||||
Constituenta,
|
||||
CstType,
|
||||
User,
|
||||
_empty_term, _empty_definition
|
||||
)
|
||||
|
||||
|
||||
class TestConstituenta(TestCase):
|
||||
def setUp(self):
|
||||
self.schema1 = RSForm.objects.create(title='Test1')
|
||||
self.schema2 = RSForm.objects.create(title='Test2')
|
||||
|
||||
def test_str(self):
|
||||
testStr = 'X1'
|
||||
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test')
|
||||
self.assertEqual(str(cst), testStr)
|
||||
|
||||
def test_order_not_null(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias='X1', schema=self.schema1)
|
||||
|
||||
def test_order_positive_integer(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias='X1', schema=self.schema1, order=-1)
|
||||
|
||||
def test_order_min_value(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
cst = Constituenta.objects.create(alias='X1', schema=self.schema1, order=0)
|
||||
cst.full_clean()
|
||||
|
||||
def test_schema_not_null(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias='X1', order=1)
|
||||
|
||||
def test_alias_unique(self):
|
||||
alias = 'X1'
|
||||
|
||||
original = Constituenta.objects.create(alias=alias, order=1, schema=self.schema1)
|
||||
self.assertIsNotNone(original)
|
||||
|
||||
clone = Constituenta.objects.create(alias=alias, order=2, schema=self.schema2)
|
||||
self.assertNotEqual(clone, original)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias=alias, order=1, schema=self.schema1)
|
||||
|
||||
def test_order_unique(self):
|
||||
original = Constituenta.objects.create(alias='X1', order=1, schema=self.schema1)
|
||||
self.assertIsNotNone(original)
|
||||
|
||||
clone = Constituenta.objects.create(alias='X2', order=1, schema=self.schema2)
|
||||
self.assertNotEqual(clone, original)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias='X2', order=1, schema=self.schema1)
|
||||
|
||||
def test_create_default(self):
|
||||
cst = Constituenta.objects.create(
|
||||
alias='X1',
|
||||
schema=self.schema1,
|
||||
order=1
|
||||
)
|
||||
self.assertEqual(cst.schema, self.schema1)
|
||||
self.assertEqual(cst.order, 1)
|
||||
self.assertEqual(cst.alias, 'X1')
|
||||
self.assertEqual(cst.csttype, CstType.BASE)
|
||||
self.assertEqual(cst.convention, '')
|
||||
self.assertEqual(cst.definition_formal, '')
|
||||
self.assertEqual(cst.term, _empty_term())
|
||||
self.assertEqual(cst.definition_text, _empty_definition())
|
||||
|
||||
def test_create(self):
|
||||
cst = Constituenta.objects.create(
|
||||
alias='S1',
|
||||
schema=self.schema1,
|
||||
order=1,
|
||||
csttype=CstType.STRUCTURED,
|
||||
convention='Test convention',
|
||||
definition_formal='X1=X1',
|
||||
term={'raw': 'Текст @{12|3}', 'resolved': 'Текст тест', 'forms': []},
|
||||
definition_text={'raw': 'Текст1 @{12|3}', 'resolved': 'Текст1 тест'},
|
||||
)
|
||||
self.assertEqual(cst.schema, self.schema1)
|
||||
self.assertEqual(cst.order, 1)
|
||||
self.assertEqual(cst.alias, 'S1')
|
||||
self.assertEqual(cst.csttype, CstType.STRUCTURED)
|
||||
self.assertEqual(cst.convention, 'Test convention')
|
||||
self.assertEqual(cst.definition_formal, 'X1=X1')
|
||||
self.assertEqual(cst.term, {'raw': 'Текст @{12|3}', 'resolved': 'Текст тест', 'forms': []})
|
||||
self.assertEqual(cst.definition_text, {'raw': 'Текст1 @{12|3}', 'resolved': 'Текст1 тест'})
|
||||
|
||||
|
||||
class TestRSForm(TestCase):
|
||||
def setUp(self):
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.assertNotEqual(self.user1, self.user2)
|
||||
|
||||
def test_str(self):
|
||||
testStr = 'Test123'
|
||||
schema = RSForm.objects.create(title=testStr, owner=self.user1, alias='КС1')
|
||||
self.assertEqual(str(schema), testStr)
|
||||
|
||||
def test_create_default(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
self.assertIsNone(schema.owner)
|
||||
self.assertEqual(schema.title, 'Test')
|
||||
self.assertEqual(schema.alias, '')
|
||||
self.assertEqual(schema.comment, '')
|
||||
self.assertEqual(schema.is_common, False)
|
||||
|
||||
def test_create(self):
|
||||
schema = RSForm.objects.create(
|
||||
title='Test',
|
||||
owner=self.user1,
|
||||
alias='KS1',
|
||||
comment='Test comment',
|
||||
is_common=True
|
||||
)
|
||||
self.assertEqual(schema.owner, self.user1)
|
||||
self.assertEqual(schema.title, 'Test')
|
||||
self.assertEqual(schema.alias, 'KS1')
|
||||
self.assertEqual(schema.comment, 'Test comment')
|
||||
self.assertEqual(schema.is_common, True)
|
||||
|
||||
def test_constituents(self):
|
||||
schema1 = RSForm.objects.create(title='Test1')
|
||||
schema2 = RSForm.objects.create(title='Test2')
|
||||
self.assertFalse(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
|
||||
Constituenta.objects.create(alias='X1', schema=schema1, order=1)
|
||||
Constituenta.objects.create(alias='X2', schema=schema1, order=2)
|
||||
self.assertTrue(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
self.assertEqual(schema1.constituents().count(), 2)
|
||||
|
||||
def test_insert_at(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
cst1 = schema.insert_at(1, 'X1', CstType.BASE)
|
||||
self.assertEqual(cst1.order, 1)
|
||||
self.assertEqual(cst1.schema, schema)
|
||||
|
||||
cst2 = schema.insert_at(1, 'X2', CstType.BASE)
|
||||
cst1.refresh_from_db()
|
||||
self.assertEqual(cst2.order, 1)
|
||||
self.assertEqual(cst2.schema, schema)
|
||||
self.assertEqual(cst1.order, 2)
|
||||
|
||||
cst3 = schema.insert_at(4, 'X3', CstType.BASE)
|
||||
cst2.refresh_from_db()
|
||||
cst1.refresh_from_db()
|
||||
self.assertEqual(cst3.order, 4)
|
||||
self.assertEqual(cst3.schema, schema)
|
||||
self.assertEqual(cst2.order, 1)
|
||||
self.assertEqual(cst1.order, 2)
|
||||
|
||||
cst4 = schema.insert_at(3, 'X4', CstType.BASE)
|
||||
cst3.refresh_from_db()
|
||||
cst2.refresh_from_db()
|
||||
cst1.refresh_from_db()
|
||||
self.assertEqual(cst4.order, 3)
|
||||
self.assertEqual(cst4.schema, schema)
|
||||
self.assertEqual(cst3.order, 5)
|
||||
self.assertEqual(cst2.order, 1)
|
||||
self.assertEqual(cst1.order, 2)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
schema.insert_at(0, 'X5', CstType.BASE)
|
||||
|
||||
def test_insert_last(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
cst1 = schema.insert_last('X1', CstType.BASE)
|
||||
self.assertEqual(cst1.order, 1)
|
||||
self.assertEqual(cst1.schema, schema)
|
||||
|
||||
cst2 = schema.insert_last('X2', CstType.BASE)
|
||||
self.assertEqual(cst2.order, 2)
|
||||
self.assertEqual(cst2.schema, schema)
|
||||
self.assertEqual(cst1.order, 1)
|
||||
|
||||
def test_to_json(self):
|
||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
||||
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
||||
expected = json.loads(
|
||||
f'{{"type": "rsform", "title": "Test", "alias": "KS1", '
|
||||
f'"comment": "Test", "items": '
|
||||
f'[{{"entityUID": {x2.id}, "type": "constituenta", "cstType": "basic", "alias": "X2", "convention": "", '
|
||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}, '
|
||||
f'{{"entityUID": {x1.id}, "type": "constituenta", "cstType": "basic", "alias": "X1", "convention": "", '
|
||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
||||
)
|
||||
self.assertEqual(schema.to_json(), expected)
|
||||
|
||||
def test_import_json(self):
|
||||
input = json.loads(
|
||||
'{"type": "rsform", "title": "Test", "alias": "KS1", '
|
||||
'"comment": "Test", "items": '
|
||||
'[{"entityUID": 1337, "type": "constituenta", "cstType": "basic", "alias": "X1", "convention": "", '
|
||||
'"term": {"raw": "", "resolved": ""}, '
|
||||
'"definition": {"formal": "123", "text": {"raw": "", "resolved": ""}}}, '
|
||||
'{"entityUID": 55, "type": "constituenta", "cstType": "basic", "alias": "X2", "convention": "", '
|
||||
'"term": {"raw": "", "resolved": ""}, '
|
||||
'"definition": {"formal": "", "text": {"raw": "", "resolved": ""}}}]}'
|
||||
)
|
||||
schema = RSForm.import_json(self.user1, input, False)
|
||||
self.assertEqual(schema.owner, self.user1)
|
||||
self.assertEqual(schema.title, 'Test')
|
||||
self.assertEqual(schema.alias, 'KS1')
|
||||
self.assertEqual(schema.is_common, False)
|
||||
constituents = schema.constituents().order_by('order')
|
||||
self.assertEqual(constituents.count(), 2)
|
||||
self.assertEqual(constituents[0].alias, 'X1')
|
||||
self.assertEqual(constituents[0].definition_formal, '123')
|
19
rsconcept/backend/apps/rsform/tests/t_serializers.py
Normal file
19
rsconcept/backend/apps/rsform/tests/t_serializers.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
''' Testing serializers '''
|
||||
from django.test import TestCase
|
||||
from apps.rsform.serializers import ExpressionSerializer
|
||||
|
||||
|
||||
class TestExpressionSerializer(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_validate(self):
|
||||
serializer = ExpressionSerializer(data={'expression': 'X1=X1'})
|
||||
self.assertTrue(serializer.is_valid(raise_exception=False))
|
||||
self.assertEqual(serializer.validated_data['expression'], 'X1=X1')
|
||||
|
||||
def test_missing_data(self):
|
||||
serializer = ExpressionSerializer(data={})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||
serializer = ExpressionSerializer(data={'schema': 1})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
197
rsconcept/backend/apps/rsform/tests/t_views.py
Normal file
197
rsconcept/backend/apps/rsform/tests/t_views.py
Normal file
|
@ -0,0 +1,197 @@
|
|||
''' Testing views '''
|
||||
import json
|
||||
import os
|
||||
import io
|
||||
from zipfile import ZipFile
|
||||
from rest_framework.test import APITestCase, APIRequestFactory, APIClient
|
||||
from rest_framework.exceptions import ErrorDetail
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.rsform.models import Syntax, RSForm, CstType
|
||||
from apps.rsform.views import (
|
||||
convert_to_ascii,
|
||||
convert_to_math,
|
||||
parse_expression
|
||||
)
|
||||
|
||||
|
||||
class TestRSFormViewset(APITestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create(username='UserTest')
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
self.rsform_owned: RSForm = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
|
||||
self.rsform_unowned: RSForm = RSForm.objects.create(title='Test2', alias='T2')
|
||||
|
||||
def test_create_anonymous(self):
|
||||
self.client.logout()
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post('/api/rsforms/', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_create_populate_user(self):
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post('/api/rsforms/', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['title'], 'Title')
|
||||
self.assertEqual(response.data['owner'], self.user.id)
|
||||
|
||||
def test_update(self):
|
||||
data = json.dumps({'id': self.rsform_owned.id, 'title': 'New title'})
|
||||
response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['title'], 'New title')
|
||||
self.assertEqual(response.data['alias'], self.rsform_owned.alias)
|
||||
|
||||
def test_update_unowned(self):
|
||||
data = json.dumps({'id': self.rsform_unowned.id, 'title': 'New title'})
|
||||
response = self.client.patch(f'/api/rsforms/{self.rsform_unowned.id}/',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_destroy(self):
|
||||
response = self.client.delete(f'/api/rsforms/{self.rsform_owned.id}/')
|
||||
self.assertTrue(response.status_code in [202, 204])
|
||||
|
||||
def test_destroy_admin_override(self):
|
||||
response = self.client.delete(f'/api/rsforms/{self.rsform_unowned.id}/')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
response = self.client.delete(f'/api/rsforms/{self.rsform_unowned.id}/')
|
||||
self.assertTrue(response.status_code in [202, 204])
|
||||
|
||||
def test_contents(self):
|
||||
schema = RSForm.objects.create(title='Title1')
|
||||
schema.insert_last(alias='X1', type=CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.id}/contents/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, schema.to_json())
|
||||
|
||||
def test_details(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
schema.insert_at(1, 'X1', CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.id}/details/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['title'], 'Test')
|
||||
self.assertEqual(len(response.data['items']), 1)
|
||||
self.assertEqual(response.data['items'][0]['parse']['status'], 'verified')
|
||||
|
||||
def test_check(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
schema.insert_at(1, 'X1', CstType.BASE)
|
||||
data = json.dumps({'expression': 'X1=X1'})
|
||||
response = self.client.post(f'/api/rsforms/{schema.id}/check/', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['parseResult'], True)
|
||||
self.assertEqual(response.data['syntax'], Syntax.MATH)
|
||||
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
|
||||
self.assertEqual(response.data['typification'], 'LOGIC')
|
||||
self.assertEqual(response.data['valueClass'], 'value')
|
||||
|
||||
def test_import_trs(self):
|
||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file}
|
||||
response = self.client.post('/api/rsforms/import-trs/', data=data, format='multipart')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertTrue(response.data['title'] != '')
|
||||
|
||||
def test_export_trs(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
schema.insert_at(1, 'X1', CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.id}/export-trs/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
|
||||
with io.BytesIO(response.content) as stream:
|
||||
with ZipFile(stream, 'r') as zipped_file:
|
||||
self.assertIsNone(zipped_file.testzip())
|
||||
self.assertIn('document.json', zipped_file.namelist())
|
||||
|
||||
def test_claim(self):
|
||||
response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/')
|
||||
self.assertEqual(response.status_code, 304)
|
||||
response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/claim/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.rsform_unowned.refresh_from_db()
|
||||
self.assertEqual(self.rsform_unowned.owner, self.user)
|
||||
|
||||
def test_claim_anonymous(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestFunctionalViews(APITestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create(username='UserTest')
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_rsform(self):
|
||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
|
||||
response = self.client.post('/api/rsforms/create-detailed/', data=data, format='multipart')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['title'], 'Test123')
|
||||
self.assertEqual(response.data['alias'], 'ks1')
|
||||
self.assertEqual(response.data['comment'], '123')
|
||||
|
||||
def test_create_rsform_fallback(self):
|
||||
data = {'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
|
||||
response = self.client.post('/api/rsforms/create-detailed/', data=data)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['title'], 'Test123')
|
||||
self.assertEqual(response.data['alias'], 'ks1')
|
||||
self.assertEqual(response.data['comment'], '123')
|
||||
|
||||
def test_convert_to_ascii(self):
|
||||
data = {'expression': '1=1'}
|
||||
request = self.factory.post('/api/func/to-ascii', data)
|
||||
response = convert_to_ascii(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['result'], r'1 \eq 1')
|
||||
|
||||
def test_convert_to_ascii_missing_data(self):
|
||||
data = {'data': '1=1'}
|
||||
request = self.factory.post('/api/func/to-ascii', data)
|
||||
response = convert_to_ascii(request)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIsInstance(response.data['expression'][0], ErrorDetail)
|
||||
|
||||
def test_convert_to_math(self):
|
||||
data = {'expression': r'1 \eq 1'}
|
||||
request = self.factory.post('/api/func/to-math', data)
|
||||
response = convert_to_math(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['result'], r'1=1')
|
||||
|
||||
def test_convert_to_math_missing_data(self):
|
||||
data = {'data': r'1 \eq 1'}
|
||||
request = self.factory.post('/api/func/to-math', data)
|
||||
response = convert_to_math(request)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIsInstance(response.data['expression'][0], ErrorDetail)
|
||||
|
||||
def test_parse_expression(self):
|
||||
data = {'expression': r'1=1'}
|
||||
request = self.factory.post('/api/func/parse-expression', data)
|
||||
response = parse_expression(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['parseResult'], True)
|
||||
self.assertEqual(response.data['syntax'], Syntax.MATH)
|
||||
self.assertEqual(response.data['astText'], '[=[1][1]]')
|
||||
|
||||
def test_parse_expression_missing_data(self):
|
||||
data = {'data': r'1=1'}
|
||||
request = self.factory.post('/api/func/parse-expression', data)
|
||||
response = parse_expression(request)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIsInstance(response.data['expression'][0], ErrorDetail)
|
16
rsconcept/backend/apps/rsform/urls.py
Normal file
16
rsconcept/backend/apps/rsform/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
''' Routing for rsform api '''
|
||||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from . import views
|
||||
|
||||
rsform_router = routers.SimpleRouter()
|
||||
rsform_router.register(r'rsforms', views.RSFormViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('rsforms/import-trs/', views.TrsImportView.as_view()),
|
||||
path('rsforms/create-detailed/', views.create_rsform),
|
||||
path('func/parse-expression/', views.parse_expression),
|
||||
path('func/to-ascii/', views.convert_to_ascii),
|
||||
path('func/to-math/', views.convert_to_math),
|
||||
path('', include(rsform_router.urls)),
|
||||
]
|
33
rsconcept/backend/apps/rsform/utils.py
Normal file
33
rsconcept/backend/apps/rsform/utils.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
''' Utility functions '''
|
||||
import json
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
class ObjectOwnerOrAdmin(BasePermission):
|
||||
''' Permission for object ownership restriction '''
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return request.user == obj.owner or request.user.is_staff
|
||||
|
||||
|
||||
def read_trs(file) -> dict:
|
||||
''' Read JSON from TRS file '''
|
||||
# TODO: deal with different versions
|
||||
with ZipFile(file, 'r') as archive:
|
||||
json_data = archive.read('document.json')
|
||||
return json.loads(json_data)
|
||||
|
||||
|
||||
def write_trs(json_data: dict) -> bytes:
|
||||
''' Write json data to TRS file including version info '''
|
||||
json_data["claimed"] = False
|
||||
json_data["selection"] = []
|
||||
json_data["version"] = 16
|
||||
json_data["versionInfo"] = "Exteor 4.8.13.1000 - 30/05/2022"
|
||||
|
||||
content = BytesIO()
|
||||
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
||||
with ZipFile(content, 'w') as archive:
|
||||
archive.writestr('document.json', data=data)
|
||||
return content.getvalue()
|
169
rsconcept/backend/apps/rsform/views.py
Normal file
169
rsconcept/backend/apps/rsform/views.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
import json
|
||||
from django.http import HttpResponse
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import views, viewsets, filters
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework import permissions
|
||||
|
||||
import pyconcept
|
||||
from . import models
|
||||
from . import serializers
|
||||
from . import utils
|
||||
|
||||
|
||||
class RSFormViewSet(viewsets.ModelViewSet):
|
||||
queryset = models.RSForm.objects.all()
|
||||
serializer_class = serializers.RSFormSerializer
|
||||
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_fields = ['owner', 'is_common']
|
||||
ordering_fields = ('owner', 'title', 'time_update')
|
||||
ordering = ('-time_update')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
|
||||
return serializer.save(owner=self.request.user)
|
||||
else:
|
||||
return serializer.save()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['update', 'destroy', 'partial_update']:
|
||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
||||
elif self.action in ['create', 'claim']:
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
else:
|
||||
permission_classes = [permissions.AllowAny]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def claim(self, request, pk=None):
|
||||
schema: models.RSForm = self.get_object()
|
||||
if schema.owner == self.request.user:
|
||||
return Response(status=304)
|
||||
else:
|
||||
schema.owner = self.request.user
|
||||
schema.save()
|
||||
return Response(status=200)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def contents(self, request, pk):
|
||||
''' View schema contents (including constituents) '''
|
||||
schema = self.get_object().to_json()
|
||||
return Response(schema)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def details(self, request, pk):
|
||||
''' Detailed schema view including statuses '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
result = pyconcept.check_schema(json.dumps(schema.to_json()))
|
||||
output_data = json.loads(result)
|
||||
output_data['id'] = schema.id
|
||||
output_data['time_update'] = schema.time_update
|
||||
output_data['time_create'] = schema.time_create
|
||||
output_data['is_common'] = schema.is_common
|
||||
output_data['owner'] = (schema.owner.pk if schema.owner is not None else None)
|
||||
return Response(output_data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def check(self, request, pk):
|
||||
''' Check RS expression against schema context '''
|
||||
schema = self.get_object().to_json()
|
||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
result = pyconcept.check_expression(json.dumps(schema), expression)
|
||||
return Response(json.loads(result))
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||
def export_trs(self, request, pk):
|
||||
''' Download Exteor compatible file '''
|
||||
schema = self.get_object().to_json()
|
||||
trs = utils.write_trs(schema)
|
||||
filename = self.get_object().alias
|
||||
if filename == '' or not filename.isascii():
|
||||
# Note: non-ascii symbols in Content-Disposition
|
||||
# are not supported by some browsers
|
||||
filename = 'Schema'
|
||||
filename += '.trs'
|
||||
|
||||
response = HttpResponse(trs, content_type='application/zip')
|
||||
response['Content-Disposition'] = f'attachment; filename={filename}'
|
||||
return response
|
||||
|
||||
|
||||
class TrsImportView(views.APIView):
|
||||
''' Upload RS form in Exteor format '''
|
||||
serializer_class = serializers.FileSerializer
|
||||
|
||||
def post(self, request, format=None):
|
||||
data = utils.read_trs(request.FILES['file'].file)
|
||||
owner = self.request.user
|
||||
if owner.is_anonymous:
|
||||
owner = None
|
||||
schema = models.RSForm.import_json(owner, data)
|
||||
result = serializers.RSFormSerializer(schema)
|
||||
return Response(status=201, data=result.data)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def create_rsform(request):
|
||||
''' Create RSForm from user input and/or trs file '''
|
||||
owner = request.user
|
||||
if owner.is_anonymous:
|
||||
owner = None
|
||||
if ('file' not in request.FILES):
|
||||
serializer = serializers.RSFormSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema = models.RSForm.objects.create(
|
||||
title=request.data['title'],
|
||||
owner=owner,
|
||||
alias=request.data.get('alias', ''),
|
||||
comment=request.data.get('comment', ''),
|
||||
is_common=request.data.get('is_common', False),
|
||||
)
|
||||
else:
|
||||
data = utils.read_trs(request.FILES['file'].file)
|
||||
if ('title' in request.data and request.data['title'] != ''):
|
||||
data['title'] = request.data['title']
|
||||
if ('alias' in request.data and request.data['alias'] != ''):
|
||||
data['alias'] = request.data['alias']
|
||||
if ('comment' in request.data and request.data['comment'] != ''):
|
||||
data['comment'] = request.data['comment']
|
||||
is_common = True
|
||||
if ('is_common' in request.data):
|
||||
is_common = request.data['is_common']
|
||||
schema = models.RSForm.import_json(owner, data, is_common)
|
||||
result = serializers.RSFormSerializer(schema)
|
||||
return Response(status=201, data=result.data)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def parse_expression(request):
|
||||
'''Parse RS expression '''
|
||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
result = pyconcept.parse_expression(expression)
|
||||
return Response(json.loads(result))
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def convert_to_ascii(request):
|
||||
''' Convert to ASCII syntax '''
|
||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
result = pyconcept.convert_to_ascii(expression)
|
||||
return Response({'result': result})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def convert_to_math(request):
|
||||
'''Convert to MATH syntax '''
|
||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
result = pyconcept.convert_to_math(expression)
|
||||
return Response({'result': result})
|
0
rsconcept/backend/apps/users/__init__.py
Normal file
0
rsconcept/backend/apps/users/__init__.py
Normal file
3
rsconcept/backend/apps/users/admin.py
Normal file
3
rsconcept/backend/apps/users/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
rsconcept/backend/apps/users/apps.py
Normal file
6
rsconcept/backend/apps/users/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
0
rsconcept/backend/apps/users/migrations/__init__.py
Normal file
0
rsconcept/backend/apps/users/migrations/__init__.py
Normal file
1
rsconcept/backend/apps/users/models.py
Normal file
1
rsconcept/backend/apps/users/models.py
Normal file
|
@ -0,0 +1 @@
|
|||
from django.contrib.auth.models import User
|
108
rsconcept/backend/apps/users/serializers.py
Normal file
108
rsconcept/backend/apps/users/serializers.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from rest_framework import serializers
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
''' User authentification by login/password. '''
|
||||
username = serializers.CharField(
|
||||
label='Имя пользователя',
|
||||
write_only=True
|
||||
)
|
||||
password = serializers.CharField(
|
||||
label='Пароль',
|
||||
style={'input_type': 'password'},
|
||||
trim_whitespace=False,
|
||||
write_only=True
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get('username')
|
||||
password = attrs.get('password')
|
||||
if username and password:
|
||||
user = authenticate(
|
||||
request=self.context.get('request'),
|
||||
username=username,
|
||||
password=password
|
||||
)
|
||||
if not user:
|
||||
msg = 'Неправильное сочетание имени пользователя и пароля.'
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
else:
|
||||
msg = 'Заполните оба поля: Имя пользователя и Пароль.'
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
''' Authentication data serializaer '''
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'is_staff'
|
||||
]
|
||||
|
||||
|
||||
class UserInfoSerializer(serializers.ModelSerializer):
|
||||
''' User data serializaer '''
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
''' User data serializaer '''
|
||||
id = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
]
|
||||
|
||||
|
||||
class SignupSerializer(serializers.ModelSerializer):
|
||||
''' User profile create '''
|
||||
id = serializers.IntegerField(read_only=True)
|
||||
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
|
||||
password2 = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'password',
|
||||
'password2'
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password2']:
|
||||
raise serializers.ValidationError({"password": "Введенные пароли не совпадают"})
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
user = models.User.objects.create_user(
|
||||
validated_data['username'], validated_data['email'], validated_data['password']
|
||||
)
|
||||
user.first_name = validated_data['first_name']
|
||||
user.last_name = validated_data['last_name']
|
||||
user.save()
|
||||
return user
|
3
rsconcept/backend/apps/users/tests/__init__.py
Normal file
3
rsconcept/backend/apps/users/tests/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# flake8: noqa
|
||||
from .t_views import *
|
||||
from .t_serializers import *
|
31
rsconcept/backend/apps/users/tests/t_serializers.py
Normal file
31
rsconcept/backend/apps/users/tests/t_serializers.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
''' Testing serializers '''
|
||||
from rest_framework.test import APITestCase, APIRequestFactory, APIClient
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.users.serializers import LoginSerializer
|
||||
|
||||
|
||||
class TestLoginSerializer(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username='UserTest', password='123')
|
||||
self.factory = APIRequestFactory()
|
||||
self.client = APIClient()
|
||||
|
||||
def test_validate(self):
|
||||
data = {'username': 'UserTest', 'password': '123'}
|
||||
request = self.factory.post('/users/api/login', data)
|
||||
serializer = LoginSerializer(data=data, context={'request': request})
|
||||
self.assertTrue(serializer.is_valid(raise_exception=True))
|
||||
self.assertEqual(serializer.validated_data['user'], self.user)
|
||||
|
||||
def test_validate_invalid_password(self):
|
||||
data = {'username': 'UserTest', 'password': 'invalid'}
|
||||
request = self.factory.post('/users/api/login', data)
|
||||
serializer = LoginSerializer(data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||
|
||||
def test_validate_invalid_request(self):
|
||||
data = {'username': 'UserTest', 'auth': 'invalid'}
|
||||
request = self.factory.post('/users/api/login', data)
|
||||
serializer = LoginSerializer(data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
89
rsconcept/backend/apps/users/tests/t_views.py
Normal file
89
rsconcept/backend/apps/users/tests/t_views.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
''' Testing views '''
|
||||
import json
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
# TODO: test AUTH and ATIVE_USERS
|
||||
class TestUserAPIViews(APITestCase):
|
||||
def setUp(self):
|
||||
self.username = 'UserTest'
|
||||
self.email = 'test@test.com'
|
||||
self.password = 'password'
|
||||
self.user = User.objects.create_user(
|
||||
self.username, self.email, self.password
|
||||
)
|
||||
self.client = APIClient()
|
||||
|
||||
def test_login(self):
|
||||
data = json.dumps({'username': self.username, 'password': self.password})
|
||||
response = self.client.post('/users/api/login', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 202)
|
||||
|
||||
def test_logout(self):
|
||||
self.assertEqual(self.client.post('/users/api/logout').status_code, 403)
|
||||
|
||||
self.client.force_login(user=self.user)
|
||||
self.assertEqual(self.client.get('/users/api/logout').status_code, 405)
|
||||
self.assertEqual(self.client.post('/users/api/logout').status_code, 204)
|
||||
|
||||
self.assertEqual(self.client.post('/users/api/logout').status_code, 403)
|
||||
|
||||
|
||||
class TestUserUserProfileAPIView(APITestCase):
|
||||
def setUp(self):
|
||||
self.username = 'UserTest'
|
||||
self.email = 'test@test.com'
|
||||
self.password = 'password'
|
||||
self.first_name = 'John'
|
||||
self.user = User.objects.create_user(
|
||||
self.username, self.email, self.password
|
||||
)
|
||||
self.user.first_name = self.first_name
|
||||
self.user.save()
|
||||
self.client = APIClient()
|
||||
|
||||
def test_read_profile(self):
|
||||
self.assertEqual(self.client.get('/users/api/profile').status_code, 403)
|
||||
self.client.force_login(user=self.user)
|
||||
response = self.client.get('/users/api/profile')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['username'], self.username)
|
||||
self.assertEqual(response.data['email'], self.email)
|
||||
self.assertEqual(response.data['first_name'], self.first_name)
|
||||
self.assertEqual(response.data['last_name'], '')
|
||||
|
||||
def test_edit_profile(self):
|
||||
newmail = 'newmail@gmail.com'
|
||||
data = json.dumps({'email': newmail})
|
||||
response = self.client.patch('/users/api/profile', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.force_login(user=self.user)
|
||||
response = self.client.patch('/users/api/profile', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['username'], self.username)
|
||||
self.assertEqual(response.data['email'], newmail)
|
||||
|
||||
|
||||
class TestSignupAPIView(APITestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_signup(self):
|
||||
data = json.dumps({
|
||||
'username': 'TestUser',
|
||||
'email': 'email@mail.ru',
|
||||
'password': 'Test@@123',
|
||||
'password2': 'Test@@123',
|
||||
'first_name': 'firstName',
|
||||
'last_name': 'lastName',
|
||||
})
|
||||
response = self.client.post('/users/api/signup', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue('id' in response.data)
|
||||
self.assertEqual(response.data['username'], 'TestUser')
|
||||
self.assertEqual(response.data['email'], 'email@mail.ru')
|
||||
self.assertEqual(response.data['first_name'], 'firstName')
|
||||
self.assertEqual(response.data['last_name'], 'lastName')
|
13
rsconcept/backend/apps/users/urls.py
Normal file
13
rsconcept/backend/apps/users/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
''' Routing for users management '''
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('api/auth', views.AuthAPIView.as_view()),
|
||||
path('api/active-users', views.ActiveUsersView.as_view()),
|
||||
path('api/profile', views.UserProfileAPIView.as_view()),
|
||||
path('api/signup', views.SignupAPIView.as_view()),
|
||||
path('api/login', views.LoginAPIView.as_view()),
|
||||
path('api/logout', views.LogoutAPIView.as_view()),
|
||||
]
|
76
rsconcept/backend/apps/users/views.py
Normal file
76
rsconcept/backend/apps/users/views.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from django.contrib.auth import login, logout
|
||||
|
||||
from rest_framework import status, permissions, views, generics
|
||||
from rest_framework.response import Response
|
||||
|
||||
from . import serializers
|
||||
from . import models
|
||||
|
||||
|
||||
class LoginAPIView(views.APIView):
|
||||
'''
|
||||
Login user via username + password.
|
||||
'''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
serializer = serializers.LoginSerializer(
|
||||
data=self.request.data,
|
||||
context={'request': self.request}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data['user']
|
||||
login(request, user)
|
||||
return Response(None, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
|
||||
class LogoutAPIView(views.APIView):
|
||||
'''
|
||||
Logout current user.
|
||||
'''
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
logout(request)
|
||||
return Response(None, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class SignupAPIView(generics.CreateAPIView):
|
||||
'''
|
||||
Register user.
|
||||
'''
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
serializer_class = serializers.SignupSerializer
|
||||
|
||||
|
||||
class AuthAPIView(generics.RetrieveAPIView):
|
||||
'''
|
||||
Get current user authentification ID.
|
||||
'''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.AuthSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class ActiveUsersView(generics.ListAPIView):
|
||||
'''
|
||||
Get list of active user.
|
||||
'''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return models.User.objects.filter(is_active=True)
|
||||
|
||||
|
||||
class UserProfileAPIView(generics.RetrieveUpdateAPIView):
|
||||
'''
|
||||
User profile info.
|
||||
'''
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
Binary file not shown.
BIN
rsconcept/backend/data/Конструкты/K0002 Сеть процессов.trs
Normal file
BIN
rsconcept/backend/data/Конструкты/K0002 Сеть процессов.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Конструкты/K0004 Граф термов.trs
Normal file
BIN
rsconcept/backend/data/Конструкты/K0004 Граф термов.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Конструкты/K0005 Полифакторструктура.trs
Normal file
BIN
rsconcept/backend/data/Конструкты/K0005 Полифакторструктура.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Конструкты/K0006 Факторструктура.trs
Normal file
BIN
rsconcept/backend/data/Конструкты/K0006 Факторструктура.trs
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0003 Разбиение.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0003 Разбиение.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0005 Порядки.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0005 Порядки.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0007 Графы.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0007 Графы.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0008 Функции.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0008 Функции.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0009 Цепочка.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0009 Цепочка.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0011 Группа.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0011 Группа.trs
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Математические/M0015 Поле.trs
Normal file
BIN
rsconcept/backend/data/Математические/M0015 Поле.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Предметные/D0001 Генеалогия.trs
Normal file
BIN
rsconcept/backend/data/Предметные/D0001 Генеалогия.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Предметные/D0002 Мелодии.trs
Normal file
BIN
rsconcept/backend/data/Предметные/D0002 Мелодии.trs
Normal file
Binary file not shown.
BIN
rsconcept/backend/data/Предметные/D0003 Друзья и Враги.trs
Normal file
BIN
rsconcept/backend/data/Предметные/D0003 Друзья и Враги.trs
Normal file
Binary file not shown.
Binary file not shown.
BIN
rsconcept/backend/data/Предметные/D0005 Теория субъектов.trs
Normal file
BIN
rsconcept/backend/data/Предметные/D0005 Теория субъектов.trs
Normal file
Binary file not shown.
18
rsconcept/backend/entrypoint.sh
Normal file
18
rsconcept/backend/entrypoint.sh
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Before doing anything wait for database to come online
|
||||
if [ "$DB_ENGINE" = "django.db.backends.postgresql_psycopg2" ]
|
||||
then
|
||||
echo "Waiting DB..."
|
||||
|
||||
while ! nc -z $DB_HOST $DB_PORT;
|
||||
do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "Ready!"
|
||||
fi
|
||||
|
||||
python $APP_HOME/manage.py collectstatic --noinput --clear
|
||||
python $APP_HOME/manage.py migrate
|
||||
|
||||
# Execute given input command
|
||||
exec "$@"
|
22
rsconcept/backend/manage.py
Normal file
22
rsconcept/backend/manage.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
rsconcept/backend/project/__init__.py
Normal file
0
rsconcept/backend/project/__init__.py
Normal file
165
rsconcept/backend/project/settings.py
Normal file
165
rsconcept/backend/project/settings.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
Django settings for project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.1.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'not-a-secret')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.environ.get('DEBUG', True) in [True, 'True', '1']
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(';')
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'django_filters',
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
|
||||
'apps.users',
|
||||
'apps.rsform',
|
||||
]
|
||||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny'
|
||||
],
|
||||
'DEFAULT_FILTER_BACKENDS': [
|
||||
'django_filters.rest_framework.DjangoFilterBackend'
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
# TODO: use env setup to populate allowed_origins in production
|
||||
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';')
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000').split(';')
|
||||
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
LOGIN_URL = '/accounts/login/'
|
||||
LOGIN_REDIRECT_URL = '/home'
|
||||
LOGOUT_REDIRECT_URL = '/home'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [str(BASE_DIR) + '/templates/'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
|
||||
'NAME': os.environ.get('DB_NAME', BASE_DIR / 'db.sqlite3'),
|
||||
'USER': os.environ.get('DB_USER'),
|
||||
'PASSWORD': os.environ.get('DB_PASSWORD'),
|
||||
'HOST': os.environ.get('DB_HOST'),
|
||||
'DB_PORT': os.environ.get('DB_PORT'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
# NOTE: Password validators disabled
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
# },
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
# },
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
# },
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
# },
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'ru'
|
||||
TIME_ZONE = 'Europe/Moscow'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
|
||||
STATIC_URL = 'static/'
|
||||
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
|
||||
MEDIA_URL = 'media/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
17
rsconcept/backend/project/urls.py
Normal file
17
rsconcept/backend/project/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
''' Main URL router '''
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
from django.contrib import admin
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('__debug__/', include('debug_toolbar.urls')),
|
||||
path('', lambda request: redirect('docs/', permanent=True)),
|
||||
path('docs/', include_docs_urls(title='ConceptPortal API'),
|
||||
name='docs'),
|
||||
path('api/', include('apps.rsform.urls')),
|
||||
path('users/', include('apps.users.urls')),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
16
rsconcept/backend/project/wsgi.py
Normal file
16
rsconcept/backend/project/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for rsconcept project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
|
||||
application = get_wsgi_application()
|
8
rsconcept/backend/requirements.txt
Normal file
8
rsconcept/backend/requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
tzdata
|
||||
django
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
django-filter
|
||||
psycopg2-binary
|
||||
gunicorn
|
||||
coreapi
|
7
rsconcept/backend/requirements_dev.txt
Normal file
7
rsconcept/backend/requirements_dev.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
tzdata
|
||||
django
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
django-filter
|
||||
coverage
|
||||
coreapi
|
3
rsconcept/frontend/.dockerignore
Normal file
3
rsconcept/frontend/.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Dev specific
|
||||
.gitignore
|
||||
node_modules
|
23
rsconcept/frontend/.gitignore
vendored
Normal file
23
rsconcept/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
39
rsconcept/frontend/Dockerfile
Normal file
39
rsconcept/frontend/Dockerfile
Normal file
|
@ -0,0 +1,39 @@
|
|||
# ======== 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/*
|
||||
|
||||
# ======= Build =======
|
||||
FROM node-base as builder
|
||||
|
||||
ENV NODE_ENV production
|
||||
WORKDIR /result
|
||||
|
||||
# Install dependencies
|
||||
COPY *.json *.js ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Build deployment files
|
||||
COPY ./public ./public
|
||||
COPY ./src ./src
|
||||
RUN npm run build
|
||||
|
||||
# ========= Server =======
|
||||
FROM node-base as product-server
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Install serve util
|
||||
RUN npm install -g serve
|
||||
|
||||
# Setup USER
|
||||
RUN adduser --system --group app
|
||||
USER node
|
||||
|
||||
# Bring up deployment files
|
||||
WORKDIR /home/node
|
||||
COPY --chown=node:node --from=builder /result/build ./
|
||||
|
||||
# Start server through docker-compose
|
||||
# serve -s /home/node -l 3000
|
17919
rsconcept/frontend/package-lock.json
generated
Normal file
17919
rsconcept/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
rsconcept/frontend/package.json
Normal file
56
rsconcept/frontend/package.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.34",
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"axios": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-data-table-component": "^7.5.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.10",
|
||||
"react-intl": "^6.4.4",
|
||||
"react-loader-spinner": "^5.3.4",
|
||||
"react-router-dom": "^6.12.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-tabs": "^6.0.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"styled-components": "^6.0.4",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
25
rsconcept/frontend/public/favicon.svg
Normal file
25
rsconcept/frontend/public/favicon.svg
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="32px" height="32px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 8.24 8.24"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil3 {fill:none}
|
||||
.fil2 {fill:#E31E24}
|
||||
.fil1 {fill:#17A7E3}
|
||||
.fil0 {fill:#02AD02}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<path class="fil0" d="M4.19 7.09l-2.03 -3.12 -2.03 3.12 4.06 0zm-3.05 -0.54l1.02 -1.56 1.01 1.56 -2.03 0z"/>
|
||||
<path class="fil1" d="M4.19 7.09l2.02 -3.12 2.03 3.12 -4.05 0zm1.01 -0.54l1.01 -1.56 1.02 1.56 -2.03 0z"/>
|
||||
<path class="fil2" d="M2.16 3.97l2.03 -3.12 2.02 3.12 -4.05 0zm1.01 -0.52l1.02 -1.56 1.01 1.56 -2.03 0z"/>
|
||||
<rect class="fil3" width="8.24" height="8.24"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
31
rsconcept/frontend/public/index.html
Normal file
31
rsconcept/frontend/public/index.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Веб-приложение для работы с концептуальными схемами" />
|
||||
|
||||
<link rel="manifest" id="manifest-placeholder" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<title>Концепт Портал</title>
|
||||
|
||||
<script>
|
||||
if (
|
||||
localStorage.getItem('darkMode') === 'true' ||
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!('color-theme' in localStorage) &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Включите использование JavaScript для работы с данным веб-приложением.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
536
rsconcept/frontend/public/logo.svg
Normal file
536
rsconcept/frontend/public/logo.svg
Normal file
|
@ -0,0 +1,536 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1"
|
||||
id="Слой_1" xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="329.043px"
|
||||
height="287.278px" viewBox="0 0 329.043 287.278" enable-background="new 0 0 329.043 287.278" xml:space="preserve">
|
||||
<title>Drawing1</title>
|
||||
<v:documentProperties v:viewMarkup="false" v:metric="true" v:langID="1049">
|
||||
<v:userDefs>
|
||||
<v:ud v:nameU="msvConvertTheme"></v:ud>
|
||||
</v:userDefs>
|
||||
</v:documentProperties>
|
||||
<g>
|
||||
<title>Страница-1</title>
|
||||
<v:pageProperties v:shadowOffsetY="-9" v:shadowOffsetX="9" v:drawingUnits="19" v:pageScale="1" v:drawingScale="1">
|
||||
</v:pageProperties>
|
||||
<g id="shape1-1" transform="translate(0.5,-32.74)">
|
||||
<title>Лист.1</title>
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="M55.971,72.11H0
|
||||
V287.28h52.65v-13.27h-38.97V85.79h42.29V72.11L55.971,72.11z"/>
|
||||
</g>
|
||||
<g id="shape2-3" transform="translate(277.538,-24.811)">
|
||||
<title>Лист.2</title>
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="M28.861,259.8
|
||||
v25.82c0,0.92-0.74,1.66-1.66,1.66c-0.26,0-0.521-0.07-0.76-0.19l-25.55-13.1c-0.811-0.41-1.12-1.41-0.711-2.22
|
||||
c0.16-0.33,0.421-0.57,0.73-0.75l25.55-12.711c0.83-0.42,1.81-0.069,2.23,0.74C28.79,259.28,28.861,259.54,28.861,259.8z"/>
|
||||
</g>
|
||||
<g id="shape3-5" transform="translate(245.107,-32.2218)">
|
||||
<title>Лист.3</title>
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="M63.541,273.291
|
||||
v13.989H83.44l-0.109-215.9H0.001v13.99l69.75-0.1L69.7,273.291H63.541L63.541,273.291z"/>
|
||||
</g>
|
||||
<g id="shape4-7" transform="translate(276.89,-255.412)">
|
||||
<title>Лист.4</title>
|
||||
<desc>тм</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="52.11" height="26.8667" cy="273.846" cx="26.055"></v:textRect>
|
||||
<path fill="none" d="M52.11,260.41H0.001v26.87H52.11V260.41"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M20.31,270.07h-4.979v12.78h-5.67v-12.78h-4.98v-4.5h15.63V270.07z"/>
|
||||
<path d="M22.11,282.85v-17.28h5.67c0.859,0.7,1.67,1.53,2.43,2.49s1.44,1.95,2.041,2.97c0.34,0.561,0.64,1.115,0.899,1.665
|
||||
c0.26,0.551,0.49,1.075,0.69,1.575h0.06c0.34-0.88,0.705-1.689,1.095-2.43c0.391-0.74,0.795-1.42,1.215-2.04
|
||||
c0.841-1.24,1.625-2.215,2.355-2.925s1.195-1.146,1.395-1.306h5.671v17.28H39.96v-9.57c-0.619,0.54-1.189,1.23-1.709,2.07
|
||||
c-0.521,0.84-0.98,1.71-1.381,2.61c-0.22,0.52-0.415,1.034-0.584,1.545c-0.171,0.51-0.315,0.985-0.436,1.425h-3.96
|
||||
c-0.101-0.4-0.226-0.825-0.375-1.275c-0.149-0.449-0.315-0.895-0.495-1.335c-0.42-1-0.91-1.954-1.47-2.865
|
||||
c-0.561-0.909-1.15-1.635-1.771-2.175v9.57H22.11z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape37-11" transform="translate(87.5719,-196.929)">
|
||||
<title>Лист.37</title>
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="M5.85,287.28
|
||||
h45.5l7.601-13.98l-45.16-0.02v-53.91H140.3v53.91H95.071l7.54,13.98l44.479,0.02c3.65,0.106,6.759-2.635,7.11-6.27v-68.43
|
||||
c0.14-3.508-2.555-6.483-6.061-6.69H5.951c-3.301,0.249-5.874,2.961-5.95,6.27v69.06C0.294,284.36,2.723,286.877,5.85,287.28z"/>
|
||||
</g>
|
||||
<g id="shape5-13" transform="translate(56.7459,-39.9262)">
|
||||
<title>Лист.5</title>
|
||||
|
||||
<path fill="none" stroke="#283E59" stroke-width="11.9056" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M107.79,87.45l108,199.83H0L107.79,87.45L107.79,87.45"/>
|
||||
</g>
|
||||
<g id="shape6-16" transform="translate(58.5424,-227.821)">
|
||||
<title>Лист.6</title>
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="M0.001,262.82
|
||||
v22.8c0,0.92,0.739,1.66,1.66,1.66c0.27,0,0.529-0.07,0.76-0.19l22.56-11.56c0.811-0.41,1.12-1.41,0.71-2.23
|
||||
c-0.16-0.32-0.42-0.57-0.75-0.74L2.4,261.33c-0.81-0.39-1.81-0.07-2.21,0.75C0.07,262.32,0.001,262.56,0.001,262.82z"/>
|
||||
</g>
|
||||
<g id="shape7-18" transform="translate(124.047,-113.792)">
|
||||
<title>Лист.7</title>
|
||||
|
||||
<path fill="none" stroke="#000000" stroke-width="0.6976" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M0,287.07l41.25-76.28l41.25,76.491"/>
|
||||
</g>
|
||||
<g id="shape8-21" transform="translate(97.9283,-66.4599)">
|
||||
<title>Лист.8</title>
|
||||
|
||||
<path fill="#8DB1E2" stroke="#283E59" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M0.001,287.281l26.29-48.091l25.88,48.091H0.001L0.001,287.281z"/>
|
||||
</g>
|
||||
<g id="shape9-23" transform="translate(97.9283,-66.4599)">
|
||||
<title>Лист.9</title>
|
||||
|
||||
<path fill="none" stroke="#283E59" stroke-width="5.4412" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M0.001,287.281l26.29-48.091l25.88,48.091H0.001L0.001,287.281"/>
|
||||
</g>
|
||||
<g id="shape10-26" transform="translate(179.74,-66.7363)">
|
||||
<title>Лист.10</title>
|
||||
<path fill="#8DB1E2" d="M0,287.28l26.29-48.09l25.88,48.09H0L0,287.28z"/>
|
||||
</g>
|
||||
<g id="shape11-28" transform="translate(179.74,-66.7363)">
|
||||
<title>Лист.11</title>
|
||||
|
||||
<path fill="none" stroke="#283E59" stroke-width="5.4412" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M0,287.28l26.29-48.09l25.88,48.09H0L0,287.28"/>
|
||||
</g>
|
||||
<g id="shape12-31" transform="translate(-142.569,58.2562) rotate(-61.4)">
|
||||
<title>Лист.12</title>
|
||||
<desc>А</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="17.38" height="16.9165" cy="278.821" cx="8.68952"></v:textRect>
|
||||
<path fill="none" d="M17.38,270.36L0,270.36l0,16.92H17.38L17.38,270.36"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M10.094,272.632c0.382,0.607,0.73,1.235,1.043,1.884c0.313,0.648,0.595,1.299,0.846,1.952
|
||||
c0.458,1.185,0.827,2.345,1.107,3.479s0.494,2.158,0.643,3.073l-2.953,0c-0.01-0.055-0.019-0.104-0.028-0.147
|
||||
c-0.009-0.042-0.019-0.081-0.029-0.118c0.001-0.009,0.001-0.018,0-0.028c-0.009-0.056-0.02-0.121-0.035-0.197
|
||||
c-0.014-0.074-0.03-0.167-0.049-0.279c-0.02-0.094-0.032-0.176-0.042-0.245c-0.008-0.07-0.018-0.138-0.027-0.203
|
||||
c-0.01-0.019-0.014-0.037-0.014-0.057c0-0.018-0.005-0.037-0.015-0.055c-0.009-0.065-0.02-0.131-0.034-0.196
|
||||
c-0.015-0.064-0.032-0.14-0.049-0.223l-2.926,0c0.074-0.374,0.159-0.74,0.252-1.099c0.093-0.359,0.186-0.717,0.28-1.071l1.806,0
|
||||
c-0.066-0.261-0.14-0.518-0.224-0.77c-0.084-0.252-0.168-0.494-0.252-0.728c-0.15-0.401-0.292-0.744-0.426-1.029
|
||||
c-0.136-0.284-0.222-0.468-0.259-0.553c-0.28,0.598-0.537,1.223-0.77,1.876c-0.233,0.653-0.443,1.306-0.63,1.96
|
||||
c-0.159,0.541-0.296,1.078-0.412,1.611c-0.117,0.531-0.217,1.05-0.302,1.554l-2.955-0.001c0.112-0.587,0.246-1.199,0.4-1.833
|
||||
c0.154-0.636,0.324-1.275,0.511-1.918c0.363-1.27,0.784-2.494,1.26-3.675c0.476-1.181,0.966-2.167,1.47-2.961L10.094,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape13-35" transform="translate(-137.594,49.1353) rotate(-61.4)">
|
||||
<title>Лист.13</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.361l-11.381,0L0,287.28l11.38,0.001L11.38,270.361"/>
|
||||
</g>
|
||||
<g id="shape14-38" transform="translate(-135.383,44.9894) rotate(-61.4)">
|
||||
<title>Лист.14</title>
|
||||
<desc>Н</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="18.42" height="16.9165" cy="278.821" cx="9.20826"></v:textRect>
|
||||
<path fill="none" d="M18.42,270.361L0,270.36l0,16.92l18.421,0.001L18.42,270.361"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M7.503,272.633l0,10.388l-2.954,0l0-10.388L7.503,272.633z M13.86,272.633l0,10.388l-2.954,0l-0.001-5.124l-2.856,0.001
|
||||
l0-2.338l2.856-0.001l0.001-2.926L13.86,272.633z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape15-42" transform="translate(-130.131,35.0392) rotate(-61.4)">
|
||||
<title>Лист.15</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.361l-11.38,0l0.001,16.92l11.379,0L11.38,270.361"/>
|
||||
</g>
|
||||
<g id="shape16-45" transform="translate(-127.644,31.1697) rotate(-61.4)">
|
||||
<title>Лист.16</title>
|
||||
<desc>А</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="17.38" height="16.9165" cy="278.821" cx="8.68952"></v:textRect>
|
||||
<path fill="none" d="M17.38,270.361l-17.38-0.001l0,16.92l17.381,0L17.38,270.361"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M10.093,272.632c0.382,0.607,0.73,1.235,1.043,1.884c0.313,0.648,0.595,1.299,0.846,1.952
|
||||
c0.458,1.185,0.827,2.345,1.107,3.479c0.28,1.133,0.494,2.158,0.643,3.073l-2.953,0c-0.01-0.055-0.019-0.104-0.028-0.147
|
||||
c-0.009-0.042-0.019-0.081-0.029-0.118c0.001-0.009,0.001-0.018,0-0.028c-0.009-0.056-0.02-0.121-0.035-0.197
|
||||
c-0.014-0.074-0.03-0.167-0.049-0.279c-0.02-0.094-0.032-0.176-0.042-0.245c-0.008-0.07-0.018-0.138-0.027-0.203
|
||||
c-0.01-0.019-0.014-0.037-0.014-0.057c0-0.018-0.005-0.037-0.015-0.055c-0.009-0.065-0.02-0.131-0.034-0.196
|
||||
c-0.015-0.064-0.032-0.14-0.049-0.223l-2.926,0c0.074-0.374,0.159-0.74,0.252-1.099c0.093-0.359,0.186-0.717,0.28-1.071l1.806,0
|
||||
c-0.066-0.261-0.14-0.518-0.224-0.77c-0.084-0.252-0.168-0.494-0.252-0.728c-0.15-0.401-0.292-0.744-0.426-1.029
|
||||
c-0.136-0.284-0.222-0.468-0.259-0.553c-0.28,0.598-0.537,1.223-0.77,1.876c-0.233,0.653-0.443,1.306-0.63,1.96
|
||||
c-0.159,0.541-0.296,1.078-0.412,1.611c-0.117,0.531-0.217,1.05-0.302,1.554l-2.955-0.001c0.112-0.587,0.246-1.199,0.4-1.833
|
||||
c0.154-0.636,0.324-1.275,0.511-1.918c0.363-1.27,0.784-2.494,1.26-3.675c0.476-1.181,0.966-2.167,1.47-2.961L10.093,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape17-49" transform="translate(-122.945,22.0488) rotate(-61.4)">
|
||||
<title>Лист.17</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36l-11.379,0l0,16.92l11.379,0L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape18-52" transform="translate(-120.458,17.9029) rotate(-61.4)">
|
||||
<title>Лист.18</title>
|
||||
<desc>Л</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="18.06" height="16.9165" cy="278.821" cx="9.02993"></v:textRect>
|
||||
<path fill="none" d="M18.06,270.36L0,270.361L0,287.281l18.06-0.001L18.06,270.36"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M10.228,274.956c-0.159,0-0.362,0.014-0.609,0.043c-0.248,0.027-0.502,0.102-0.764,0.223
|
||||
c-0.42,0.205-0.805,0.595-1.155,1.169c-0.35,0.574-0.525,1.487-0.525,2.738c0,0.279,0.007,0.607,0.021,0.979
|
||||
c0.013,0.373,0.035,0.752,0.063,1.134c0.017,0.318,0.041,0.63,0.069,0.939c0.028,0.307,0.056,0.588,0.084,0.84l-2.982,0
|
||||
c-0.13-1.045-0.212-1.878-0.244-2.498c-0.033-0.621-0.049-1.09-0.049-1.408c0-2.033,0.336-3.509,1.007-4.423
|
||||
c0.673-0.914,1.41-1.512,2.212-1.792c0.29-0.102,0.579-0.172,0.868-0.21c0.289-0.036,0.56-0.055,0.812-0.055l4.495,0l0,10.388
|
||||
l-2.954,0l0.001-8.063L10.228,274.956z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape19-56" transform="translate(-115.483,8.22917) rotate(-61.4)">
|
||||
<title>Лист.19</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36l-11.38,0L0,287.281l11.381-0.001L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape20-59" transform="translate(-112.995,4.08329) rotate(-61.4)">
|
||||
<title>Лист.20</title>
|
||||
<desc>И</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="18.44" height="16.9165" cy="278.821" cx="9.21636"></v:textRect>
|
||||
<path fill="none" d="M18.43,270.361H0l0,16.92l18.43,0L18.43,270.361"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M7.503,272.632l0.001,2.771c0.102-0.121,0.215-0.247,0.336-0.378s0.247-0.261,0.377-0.391
|
||||
c0.216-0.205,0.44-0.411,0.673-0.616c0.233-0.206,0.471-0.397,0.714-0.575c0.233-0.167,0.46-0.321,0.679-0.461
|
||||
c0.22-0.14,0.431-0.257,0.637-0.349l2.954,0l0,10.388l-2.954,0l0-7.434c-0.195,0.121-0.405,0.268-0.63,0.44
|
||||
c-0.224,0.173-0.448,0.358-0.672,0.553c-0.234,0.205-0.463,0.42-0.686,0.644c-0.225,0.224-0.439,0.448-0.644,0.672
|
||||
c-0.149,0.178-0.292,0.353-0.427,0.525c-0.135,0.173-0.255,0.342-0.357,0.511l0,4.088l-2.954,0l0-10.388L7.503,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape21-63" transform="translate(-107.744,-5.59045) rotate(-61.4)">
|
||||
<title>Лист.21</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36l-11.38,0l0,16.92l11.38,0L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape22-66" transform="translate(-105.533,-9.73633) rotate(-61.4)">
|
||||
<title>Лист.22</title>
|
||||
<desc>З</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="15.5" height="16.9165" cy="278.821" cx="7.74934"></v:textRect>
|
||||
<path fill="none" d="M15.5,270.361l-15.5,0l0,16.92l15.5,0L15.5,270.361"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M3.986,272.632c0.634-0.112,1.185-0.182,1.652-0.21c0.467-0.028,0.784-0.043,0.952-0.042
|
||||
c1.372,0.001,2.368,0.195,2.988,0.588c0.621,0.392,1.03,0.827,1.226,1.303c0.084,0.186,0.14,0.373,0.168,0.559
|
||||
c0.028,0.187,0.042,0.36,0.042,0.519c0,0.495-0.11,0.895-0.33,1.204c-0.219,0.307-0.459,0.541-0.721,0.7
|
||||
c-0.066,0.047-0.133,0.086-0.203,0.118c-0.07,0.034-0.138,0.063-0.203,0.092c0.112,0.037,0.243,0.091,0.392,0.161
|
||||
s0.303,0.156,0.462,0.259c0.28,0.195,0.537,0.464,0.77,0.805c0.232,0.34,0.35,0.783,0.35,1.323c0,0.85-0.383,1.601-1.148,2.253
|
||||
c-0.765,0.654-2.025,0.98-3.78,0.98c-0.102,0-0.2,0-0.294,0c-0.093,0.001-0.192-0.005-0.294-0.013
|
||||
c-0.327-0.01-0.66-0.031-1.001-0.063c-0.341-0.033-0.712-0.082-1.113-0.148l0-2.351c0.476,0.074,0.861,0.13,1.154,0.168
|
||||
c0.295,0.037,0.572,0.06,0.833,0.07c0.065,0.009,0.131,0.014,0.196,0.014s0.135,0,0.21,0c0.121,0,0.252-0.003,0.392-0.008
|
||||
c0.139-0.004,0.28-0.016,0.42-0.034c0.354-0.047,0.679-0.161,0.973-0.344c0.294-0.181,0.441-0.492,0.441-0.931
|
||||
c0-0.307-0.103-0.533-0.308-0.678c-0.205-0.145-0.434-0.24-0.686-0.288c-0.075-0.018-0.152-0.03-0.231-0.035
|
||||
c-0.08-0.005-0.157-0.007-0.231-0.006l-2.31,0l0.001-2.169l2.169,0c0.318,0,0.551-0.049,0.7-0.147
|
||||
c0.149-0.098,0.252-0.203,0.308-0.315c0.037-0.076,0.06-0.144,0.07-0.21c0.009-0.066,0.014-0.122,0.014-0.169
|
||||
c0-0.057-0.007-0.117-0.02-0.182c-0.014-0.065-0.04-0.131-0.077-0.195c-0.094-0.16-0.264-0.305-0.512-0.442
|
||||
c-0.247-0.135-0.623-0.203-1.127-0.203c-0.26,0-0.529,0.014-0.805,0.042c-0.276,0.029-0.553,0.066-0.833,0.113
|
||||
c-0.103,0.027-0.21,0.056-0.322,0.084c-0.112,0.026-0.224,0.055-0.336,0.084L3.986,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape23-70" transform="translate(431.017,-17.4753) rotate(61.4)">
|
||||
<title>Лист.23</title>
|
||||
<desc>С</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="15.49" height="16.9165" cy="278.821" cx="7.74119"></v:textRect>
|
||||
<path fill="none" d="M15.48,270.36l-15.48,0l0,16.92l15.48,0L15.48,270.36"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M11.673,274.886c-0.187-0.056-0.399-0.105-0.637-0.147c-0.238-0.042-0.492-0.063-0.763-0.063
|
||||
c-1.045,0-1.85,0.287-2.414,0.86c-0.565,0.574-0.847,1.328-0.847,2.261c-0.001,0.896,0.182,1.565,0.545,2.009
|
||||
c0.364,0.443,0.765,0.754,1.204,0.931c0.234,0.083,0.462,0.142,0.687,0.174c0.223,0.033,0.419,0.049,0.588,0.05
|
||||
c0.12,0,0.242-0.005,0.364-0.015c0.12-0.009,0.242-0.023,0.363-0.042c0.159-0.019,0.315-0.046,0.468-0.084
|
||||
c0.155-0.037,0.301-0.079,0.441-0.126l0,2.325c-0.299,0.065-0.633,0.119-1.001,0.161c-0.369,0.042-0.792,0.063-1.268,0.063
|
||||
c-1.885,0-3.236-0.506-4.053-1.519c-0.817-1.013-1.271-2.13-1.365-3.354c-0.009-0.093-0.016-0.188-0.021-0.286
|
||||
c-0.005-0.099-0.008-0.194-0.007-0.287c0-1.586,0.504-2.883,1.511-3.892c1.008-1.008,2.422-1.511,4.242-1.512
|
||||
c0.374,0,0.72,0.021,1.042,0.063c0.323,0.042,0.628,0.1,0.918,0.175L11.673,274.886z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape24-74" transform="translate(435.163,-10.2891) rotate(61.4)">
|
||||
<title>Лист.24</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36l-11.38,0l0,16.92l11.38,0L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape25-77" transform="translate(437.374,-5.86684) rotate(61.4)">
|
||||
<title>Лист.25</title>
|
||||
<desc>И</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="18.44" height="16.9165" cy="278.821" cx="9.21636"></v:textRect>
|
||||
<path fill="none" d="M18.43,270.359H0l0,16.92l18.429,0L18.43,270.359"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M7.505,272.632l-0.001,2.771c0.103-0.122,0.215-0.248,0.336-0.378c0.121-0.131,0.247-0.26,0.378-0.391
|
||||
c0.215-0.205,0.439-0.411,0.672-0.617c0.233-0.205,0.472-0.396,0.714-0.574c0.233-0.169,0.459-0.322,0.678-0.462
|
||||
c0.22-0.139,0.432-0.256,0.638-0.35l2.954,0l0,10.388l-2.954,0l0-7.434c-0.196,0.121-0.405,0.267-0.63,0.441
|
||||
c-0.224,0.173-0.448,0.357-0.672,0.553c-0.234,0.206-0.463,0.421-0.687,0.644c-0.223,0.224-0.438,0.449-0.644,0.672
|
||||
c-0.149,0.178-0.292,0.352-0.428,0.525c-0.135,0.172-0.253,0.342-0.356,0.511l0,4.088l-2.954,0l0-10.388L7.505,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape26-81" transform="translate(442.626,3.5305) rotate(61.4)">
|
||||
<title>Лист.26</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36l-11.38,0l0,16.92l11.38,0L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape27-84" transform="translate(445.113,8.22917) rotate(61.4)">
|
||||
<title>Лист.27</title>
|
||||
<desc>Н</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="18.42" height="16.9165" cy="278.821" cx="9.20826"></v:textRect>
|
||||
<path fill="none" d="M18.42,270.359l-18.42,0l0.001,16.92l18.419,0L18.42,270.359"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M7.504,272.633l0,10.388l-2.954,0l0-10.388L7.504,272.633z M13.86,272.632l0,10.388l-2.954,0l-0.001-5.124l-2.856-0.001
|
||||
l0-2.338l2.856,0.001l0-2.926L13.86,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape28-88" transform="translate(450.365,17.6265) rotate(61.4)">
|
||||
<title>Лист.28</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.381,270.36L0,270.359l0,16.92l11.381,0.001L11.381,270.36"/>
|
||||
</g>
|
||||
<g id="shape29-91" transform="translate(452.576,22.3252) rotate(61.4)">
|
||||
<title>Лист.29</title>
|
||||
<desc>Т</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="14.86" height="16.9165" cy="278.821" cx="7.42512"></v:textRect>
|
||||
<path fill="none" d="M14.85,270.359l-14.849,0l0,16.92l14.849,0L14.85,270.359"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M11.38,272.632l0.001,2.324l-2.478,0l0,8.064l-2.954,0l0-8.064l-2.478,0l0-2.324L11.38,272.632z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape30-95" transform="translate(456.445,28.9586) rotate(61.4)">
|
||||
<title>Лист.30</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.381,270.36L0,270.359l0,16.92l11.381,0.001L11.381,270.36"/>
|
||||
</g>
|
||||
<g id="shape31-98" transform="translate(458.657,33.3809) rotate(61.4)">
|
||||
<title>Лист.31</title>
|
||||
<desc>Е</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="15.23" height="16.9165" cy="278.821" cx="7.61155"></v:textRect>
|
||||
<path fill="none" d="M15.22,270.36L0,270.359l0,16.92l15.22,0.001L15.22,270.36"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M11.102,274.956l-3.807,0l0,5.739l4.074,0l0.001,2.324l-7.029,0.001l0-10.388l6.762-0.001L11.102,274.956z
|
||||
M11.733,278.134l-3.893,0l0-2.324l3.893,0L11.733,278.134z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape32-102" transform="translate(462.802,40.2907) rotate(61.4)">
|
||||
<title>Лист.32</title>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<path fill="none" d="M11.38,270.36L0,270.359l0,16.92l11.381,0.001L11.38,270.36"/>
|
||||
</g>
|
||||
<g id="shape33-105" transform="translate(465.014,44.9894) rotate(61.4)">
|
||||
<title>Лист.33</title>
|
||||
<desc>З</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="15.5" height="16.9165" cy="278.821" cx="7.74934"></v:textRect>
|
||||
<path fill="none" d="M15.5,270.359l-15.5,0l0,16.92l15.5,0L15.5,270.359"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M3.987,272.632c0.634-0.113,1.185-0.183,1.651-0.21c0.467-0.029,0.784-0.043,0.952-0.043
|
||||
c1.372,0.001,2.368,0.196,2.989,0.589c0.621,0.391,1.029,0.826,1.225,1.302c0.084,0.186,0.141,0.373,0.168,0.559
|
||||
c0.028,0.188,0.042,0.361,0.042,0.519c0,0.495-0.11,0.895-0.329,1.205c-0.22,0.307-0.46,0.541-0.721,0.7
|
||||
c-0.065,0.047-0.133,0.085-0.202,0.118c-0.07,0.033-0.139,0.063-0.203,0.092c0.112,0.038,0.241,0.092,0.392,0.162
|
||||
c0.149,0.069,0.302,0.156,0.461,0.258c0.28,0.196,0.537,0.464,0.77,0.805c0.233,0.342,0.35,0.782,0.349,1.324
|
||||
c0.001,0.849-0.382,1.6-1.147,2.253c-0.766,0.654-2.026,0.98-3.78,0.98c-0.104,0-0.201,0-0.295,0
|
||||
c-0.093,0.001-0.191-0.004-0.294-0.014c-0.326-0.01-0.66-0.03-1.001-0.062c-0.341-0.034-0.712-0.082-1.113-0.148l0-2.352
|
||||
c0.476,0.075,0.861,0.131,1.155,0.168c0.294,0.038,0.572,0.061,0.833,0.069c0.066,0.01,0.131,0.015,0.197,0.015
|
||||
c0.065,0,0.135,0,0.209,0c0.121,0,0.253-0.002,0.392-0.007c0.14-0.004,0.28-0.015,0.42-0.034
|
||||
c0.355-0.048,0.679-0.162,0.973-0.344c0.294-0.182,0.441-0.493,0.441-0.931c0.001-0.308-0.103-0.534-0.308-0.68
|
||||
c-0.205-0.144-0.433-0.24-0.686-0.287c-0.075-0.018-0.152-0.029-0.231-0.035s-0.157-0.006-0.231-0.007l-2.31,0.001l0-2.171
|
||||
l2.17,0.001c0.317-0.001,0.551-0.049,0.7-0.148c0.149-0.098,0.252-0.203,0.307-0.314c0.038-0.075,0.061-0.145,0.071-0.21
|
||||
c0.009-0.065,0.013-0.121,0.013-0.168c0.001-0.056-0.006-0.118-0.021-0.183c-0.014-0.064-0.04-0.129-0.077-0.195
|
||||
c-0.094-0.159-0.264-0.305-0.51-0.441c-0.247-0.136-0.623-0.203-1.127-0.203c-0.261,0-0.53,0.013-0.806,0.041
|
||||
c-0.275,0.028-0.552,0.066-0.832,0.112c-0.103,0.027-0.21,0.055-0.322,0.083c-0.112,0.028-0.224,0.056-0.336,0.085L3.987,272.632
|
||||
z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape34-109" transform="translate(99.9452,-40.2709)">
|
||||
<title>Лист.34</title>
|
||||
<desc>Концепт</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="131.23" height="27.53" cy="273.514" cx="65.6118"></v:textRect>
|
||||
<path fill="none" d="M131.22,259.75H0v27.53h131.22V259.75"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M28.878,265.554v2.7c0.204-0.06,0.417-0.144,0.639-0.252c0.222-0.107,0.447-0.228,0.675-0.359
|
||||
c0.444-0.252,0.87-0.559,1.278-0.918c0.408-0.36,0.744-0.75,1.008-1.171h4.554c-0.132,0.265-0.294,0.544-0.486,0.838
|
||||
s-0.408,0.591-0.648,0.891c-0.432,0.528-0.924,1.038-1.476,1.53s-1.116,0.894-1.692,1.206c0.768,0.804,1.452,1.683,2.052,2.637
|
||||
c0.6,0.954,1.109,1.923,1.53,2.907c0.24,0.563,0.447,1.128,0.621,1.691c0.174,0.564,0.315,1.116,0.423,1.656h-3.906
|
||||
c-0.205-1.14-0.555-2.193-1.053-3.159s-1.029-1.814-1.593-2.547c-0.216-0.3-0.432-0.576-0.648-0.828
|
||||
c-0.216-0.252-0.426-0.479-0.63-0.684c-0.096,0.048-0.198,0.096-0.306,0.144c-0.108,0.048-0.222,0.09-0.342,0.126v6.948H25.08
|
||||
v-13.356H28.878z"/>
|
||||
<path d="M43.422,279.144c-1.596-0.013-2.91-0.483-3.942-1.413c-1.032-0.93-1.548-2.266-1.548-4.005
|
||||
c0-1.74,0.516-3.078,1.548-4.015c1.032-0.936,2.346-1.403,3.942-1.403c1.596,0,2.91,0.468,3.942,1.403
|
||||
c1.032,0.937,1.548,2.274,1.548,4.015c0,1.739-0.516,3.075-1.548,4.005s-2.346,1.395-3.942,1.395V279.144z M43.611,276.543
|
||||
c0.066-0.006,0.135-0.015,0.207-0.026c0.396-0.072,0.759-0.31,1.089-0.712c0.33-0.401,0.495-1.095,0.495-2.078
|
||||
c0-0.984-0.165-1.677-0.495-2.079s-0.693-0.64-1.089-0.711c-0.072-0.012-0.141-0.021-0.207-0.027s-0.129-0.009-0.189-0.009
|
||||
c-0.108,0-0.237,0.012-0.387,0.036c-0.15,0.023-0.303,0.078-0.459,0.162c-0.288,0.132-0.549,0.396-0.783,0.792
|
||||
c-0.234,0.396-0.351,1.008-0.351,1.836c0,0.827,0.117,1.439,0.351,1.836c0.234,0.396,0.495,0.66,0.783,0.792
|
||||
c0.156,0.084,0.309,0.138,0.459,0.162c0.15,0.023,0.279,0.035,0.387,0.035C43.482,276.552,43.545,276.55,43.611,276.543z"/>
|
||||
<path d="M53.97,278.91h-3.402v-10.368h3.402V278.91z M60.774,278.91h-3.402v-4.392h-2.7v-2.593h2.7v-3.384h3.402V278.91z"/>
|
||||
<path d="M66.696,268.542c-0.035,0.156-0.068,0.313-0.099,0.468c-0.03,0.156-0.063,0.313-0.099,0.469
|
||||
c-0.084,0.359-0.162,0.723-0.234,1.089s-0.133,0.735-0.18,1.106c-0.049,0.313-0.084,0.618-0.108,0.918
|
||||
c-0.024,0.301-0.036,0.601-0.036,0.9c0,0.972,0.195,1.638,0.585,1.998s0.813,0.582,1.269,0.666
|
||||
c0.192,0.024,0.379,0.039,0.559,0.045s0.342,0.009,0.486,0.009c0.023,0,0.045,0,0.063,0s0.033,0,0.045,0h4.536v2.7h-5.418
|
||||
c-0.013,0-0.024,0-0.036,0c-0.313,0-0.678-0.009-1.098-0.027c-0.42-0.018-0.853-0.087-1.297-0.207
|
||||
c-0.803-0.216-1.538-0.678-2.204-1.386s-0.999-1.89-0.999-3.546c0-0.695,0.057-1.374,0.171-2.034
|
||||
c0.114-0.659,0.237-1.266,0.369-1.817c0.06-0.252,0.12-0.492,0.18-0.721c0.06-0.228,0.114-0.438,0.162-0.63H66.696z
|
||||
M72.293,275.616h-3.401v-7.074h3.401V275.616z"/>
|
||||
<path d="M84.047,278.784c-0.516,0.108-1.043,0.191-1.584,0.252c-0.539,0.06-1.128,0.09-1.764,0.09
|
||||
c-2.027,0-3.605-0.498-4.734-1.494c-1.127-0.995-1.691-2.321-1.691-3.978c0-1.788,0.555-3.126,1.665-4.014
|
||||
c1.109-0.889,2.463-1.332,4.059-1.332c0.276,0,0.579,0.018,0.909,0.054s0.663,0.102,0.999,0.198
|
||||
c0.828,0.24,1.587,0.753,2.276,1.539c0.69,0.785,1.035,2.078,1.035,3.879c0,0.048,0,0.099,0,0.152c0,0.055,0,0.105,0,0.153
|
||||
c0,0.061-0.002,0.114-0.008,0.162c-0.007,0.048-0.01,0.102-0.01,0.162h-6.408c-0.012-0.024-0.029-0.051-0.054-0.081
|
||||
s-0.042-0.063-0.054-0.1c-0.061-0.107-0.111-0.236-0.153-0.387c-0.042-0.149-0.063-0.32-0.063-0.513
|
||||
c0-0.108,0.006-0.24,0.019-0.396c0.012-0.155,0.042-0.329,0.09-0.521h3.366c-0.024-0.54-0.153-0.93-0.387-1.17
|
||||
c-0.234-0.24-0.49-0.396-0.766-0.469c-0.132-0.023-0.258-0.041-0.378-0.054c-0.12-0.012-0.233-0.018-0.342-0.018
|
||||
c-0.816,0-1.404,0.246-1.765,0.737c-0.359,0.492-0.539,1.11-0.539,1.854c0,0.061,0,0.12,0,0.18c0,0.061,0.006,0.12,0.018,0.181
|
||||
c0.061,0.672,0.352,1.29,0.873,1.854c0.521,0.564,1.473,0.846,2.854,0.846c0.443,0,0.854-0.032,1.232-0.099
|
||||
c0.378-0.065,0.747-0.165,1.107-0.297c0.035-0.012,0.068-0.024,0.099-0.036s0.063-0.023,0.099-0.036V278.784z"/>
|
||||
<path d="M96.756,278.91h-3.402v-7.776h-3.276v7.776h-3.401v-10.368h10.08V278.91z"/>
|
||||
<path d="M107.213,271.242h-2.987v7.668h-3.402v-7.668h-2.987v-2.7h9.377V271.242z"/>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape35-113" transform="translate(123.84,-114.414)">
|
||||
<title>Лист.35</title>
|
||||
<path fill="#8DB1E2" d="M0,287.28l41.46-75.87l40.841,75.87H0L0,287.28z"/>
|
||||
</g>
|
||||
<g id="shape36-115" transform="translate(123.84,-114.414)">
|
||||
<title>Лист.36</title>
|
||||
|
||||
<path fill="none" stroke="#283E59" stroke-width="5.4412" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3" d="
|
||||
M0,287.28l41.46-75.87l40.841,75.87H0L0,287.28"/>
|
||||
</g>
|
||||
<g id="shape40-118" transform="translate(56.39,5.68434E-014)">
|
||||
<title>Лист.40</title>
|
||||
<desc>Всегда вместе!</desc>
|
||||
<v:textBlock v:tabSpace="42.5197" v:margins="rect(0,0,0,0)"></v:textBlock>
|
||||
<v:textRect width="220.5" height="27.53" cy="273.514" cx="110.25"></v:textRect>
|
||||
<path fill="none" d="M220.501,259.75H0.001v27.529h220.5V259.75"/>
|
||||
<g enable-background="new ">
|
||||
<path d="M44.676,265.554c1.26,0,2.199,0.229,2.817,0.685s1.035,0.954,1.251,1.494c0.096,0.228,0.162,0.453,0.198,0.675
|
||||
c0.036,0.222,0.054,0.429,0.054,0.621c0,0.684-0.192,1.245-0.576,1.683c-0.384,0.438-0.804,0.766-1.26,0.981
|
||||
c-0.012,0-0.021,0.003-0.027,0.009c-0.006,0.006-0.015,0.009-0.027,0.009v0.036c0.996,0.349,1.656,0.837,1.98,1.467
|
||||
c0.324,0.63,0.486,1.293,0.486,1.989c0,0.924-0.372,1.773-1.116,2.547c-0.744,0.774-2.004,1.161-3.78,1.161H37.8v-13.356H44.676z
|
||||
M42.3,270.541h1.386c0.072,0,0.147-0.003,0.225-0.009s0.159-0.009,0.243-0.009c0.312-0.036,0.6-0.132,0.864-0.288
|
||||
s0.396-0.433,0.396-0.828c0-0.444-0.159-0.729-0.477-0.855s-0.669-0.194-1.053-0.207c-0.048,0-0.096,0-0.144,0
|
||||
c-0.048,0-0.096,0-0.144,0h-1.998v7.776h2.088c0.588,0,1.02-0.087,1.296-0.261c0.276-0.174,0.461-0.363,0.558-0.567
|
||||
c0.048-0.096,0.081-0.191,0.099-0.288c0.018-0.096,0.027-0.18,0.027-0.252c0-0.443-0.114-0.765-0.342-0.963
|
||||
s-0.474-0.327-0.738-0.387c-0.168-0.036-0.331-0.057-0.486-0.063c-0.156-0.006-0.288-0.009-0.396-0.009c-0.012,0-0.021,0-0.027,0
|
||||
c-0.006,0-0.015,0-0.027,0H42.3V270.541z"/>
|
||||
<path d="M58.122,271.243c-0.036-0.012-0.087-0.033-0.153-0.063c-0.066-0.029-0.147-0.063-0.243-0.099
|
||||
c-0.144-0.048-0.309-0.09-0.495-0.126s-0.381-0.054-0.585-0.054c-0.048,0-0.093,0-0.135,0s-0.087,0.006-0.135,0.018
|
||||
c-0.54,0.048-1.032,0.276-1.476,0.685c-0.444,0.407-0.666,1.134-0.666,2.178c0,0.804,0.144,1.398,0.432,1.782
|
||||
s0.606,0.647,0.954,0.792c0.204,0.084,0.405,0.138,0.603,0.162c0.198,0.023,0.363,0.035,0.495,0.035
|
||||
c0.228,0,0.435-0.012,0.621-0.035c0.186-0.024,0.375-0.066,0.567-0.126c0.06-0.024,0.126-0.052,0.198-0.081
|
||||
c0.072-0.03,0.144-0.063,0.216-0.1v2.7c-0.144,0.036-0.279,0.065-0.405,0.09s-0.25,0.048-0.369,0.072
|
||||
c-0.156,0.023-0.315,0.039-0.477,0.045s-0.339,0.009-0.531,0.009c-2.064,0-3.549-0.537-4.455-1.61
|
||||
c-0.906-1.074-1.359-2.319-1.359-3.735c0-0.145,0.002-0.285,0.009-0.423c0.006-0.138,0.021-0.279,0.045-0.423
|
||||
c0.132-1.164,0.609-2.227,1.431-3.187c0.822-0.96,2.175-1.439,4.059-1.439c0.36,0,0.666,0.015,0.918,0.045
|
||||
c0.252,0.029,0.492,0.075,0.72,0.135c0.036,0,0.072,0.006,0.108,0.018c0.036,0.013,0.072,0.024,0.108,0.036V271.243z"/>
|
||||
<path d="M68.922,278.785c-0.516,0.108-1.044,0.191-1.584,0.252c-0.54,0.06-1.128,0.09-1.764,0.09
|
||||
c-2.028,0-3.606-0.498-4.734-1.494c-1.128-0.995-1.692-2.321-1.692-3.978c0-1.788,0.555-3.126,1.665-4.014
|
||||
c1.109-0.889,2.463-1.332,4.059-1.332c0.276,0,0.579,0.018,0.909,0.054s0.663,0.102,0.999,0.198
|
||||
c0.828,0.24,1.587,0.753,2.277,1.539c0.69,0.785,1.035,2.078,1.035,3.879c0,0.048,0,0.099,0,0.152c0,0.055,0,0.105,0,0.153
|
||||
c0,0.061-0.003,0.114-0.009,0.162c-0.006,0.048-0.009,0.102-0.009,0.162h-6.408c-0.012-0.024-0.03-0.051-0.054-0.081
|
||||
c-0.024-0.03-0.042-0.063-0.054-0.1c-0.06-0.107-0.111-0.236-0.153-0.387c-0.042-0.149-0.063-0.32-0.063-0.513
|
||||
c0-0.108,0.006-0.24,0.018-0.396c0.012-0.155,0.042-0.329,0.09-0.521h3.366c-0.024-0.54-0.153-0.93-0.387-1.17
|
||||
s-0.489-0.396-0.765-0.469c-0.132-0.023-0.258-0.041-0.378-0.054c-0.12-0.012-0.234-0.018-0.342-0.018
|
||||
c-0.816,0-1.404,0.246-1.764,0.737c-0.36,0.492-0.54,1.11-0.54,1.854c0,0.061,0,0.12,0,0.18c0,0.061,0.006,0.12,0.018,0.181
|
||||
c0.06,0.672,0.351,1.29,0.873,1.854c0.522,0.564,1.473,0.846,2.853,0.846c0.444,0,0.855-0.032,1.233-0.099
|
||||
c0.378-0.065,0.747-0.165,1.107-0.297c0.036-0.012,0.069-0.024,0.099-0.036c0.03-0.012,0.063-0.023,0.099-0.036V278.785z"/>
|
||||
<path d="M78.948,271.243h-3.996v7.668H71.55v-10.368h7.398V271.243z"/>
|
||||
<path d="M89.568,276.21h1.296v2.7H78.948v-2.7h7.218v-5.076h-0.54c-0.132,0-0.282,0.01-0.45,0.027
|
||||
c-0.168,0.018-0.342,0.063-0.522,0.135c-0.336,0.145-0.645,0.421-0.927,0.828c-0.282,0.408-0.423,1.063-0.423,1.962
|
||||
c0,0.12,0.009,0.252,0.027,0.396c0.018,0.144,0.039,0.288,0.063,0.432c0,0.048,0.003,0.09,0.009,0.126
|
||||
c0.006,0.036,0.015,0.078,0.027,0.126c0.012,0.072,0.024,0.147,0.036,0.226c0.012,0.078,0.024,0.152,0.036,0.225h-3.366
|
||||
c-0.084-0.384-0.144-0.762-0.18-1.134c-0.036-0.372-0.054-0.744-0.054-1.116c0-1.5,0.285-2.583,0.855-3.249
|
||||
c0.57-0.666,1.203-1.107,1.899-1.323c0.336-0.107,0.669-0.177,0.999-0.207c0.33-0.029,0.621-0.045,0.873-0.045h5.04V276.21z"/>
|
||||
<path d="M92.934,268.542c0.612-0.072,1.152-0.126,1.62-0.162c0.468-0.036,0.918-0.054,1.35-0.054c0.036,0,0.075,0,0.117,0
|
||||
s0.081,0,0.117,0c0.312,0,0.669,0.018,1.071,0.054s0.807,0.114,1.215,0.233c0.828,0.229,1.584,0.684,2.268,1.366
|
||||
c0.684,0.683,1.026,1.772,1.026,3.271v5.66h-5.85c-0.204,0-0.486-0.012-0.846-0.036c-0.36-0.023-0.732-0.09-1.116-0.198
|
||||
c-0.564-0.168-1.08-0.471-1.548-0.908c-0.468-0.438-0.702-1.119-0.702-2.043c0-0.084,0.002-0.168,0.009-0.253
|
||||
c0.006-0.083,0.015-0.173,0.027-0.27c0.108-0.708,0.474-1.361,1.098-1.962c0.624-0.6,1.656-0.9,3.096-0.9h1.404
|
||||
c0.168,0.288,0.267,0.57,0.297,0.847c0.03,0.275,0.045,0.54,0.045,0.792c0,0.023,0,0.045,0,0.063c0,0.019,0,0.039,0,0.063v0.414
|
||||
h-0.99c-0.084,0-0.171,0.003-0.261,0.009c-0.09,0.006-0.189,0.015-0.297,0.026c-0.228,0.036-0.438,0.117-0.63,0.243
|
||||
s-0.288,0.346-0.288,0.657c0,0.264,0.099,0.475,0.297,0.63c0.198,0.156,0.465,0.234,0.801,0.234h2.052v-2.395v-0.018
|
||||
c0-0.156-0.003-0.327-0.009-0.513c-0.006-0.187-0.027-0.375-0.063-0.567c-0.084-0.479-0.318-0.921-0.702-1.323
|
||||
c-0.384-0.401-1.098-0.603-2.142-0.603c-0.156,0-0.309,0.003-0.459,0.009s-0.291,0.016-0.423,0.027
|
||||
c-0.24,0.023-0.486,0.057-0.738,0.099s-0.534,0.105-0.846,0.189V268.542z"/>
|
||||
<path d="M116.999,268.542c1.02,0,1.752,0.213,2.196,0.64c0.444,0.426,0.72,0.896,0.828,1.412
|
||||
c0.024,0.108,0.042,0.217,0.054,0.324c0.012,0.108,0.018,0.216,0.018,0.324c0,0.66-0.17,1.158-0.512,1.494
|
||||
c-0.343,0.336-0.711,0.57-1.107,0.702c-0.061,0.012-0.114,0.023-0.162,0.036c-0.049,0.012-0.096,0.023-0.145,0.035v0.036
|
||||
c0.096,0.024,0.195,0.052,0.297,0.081c0.103,0.03,0.207,0.069,0.315,0.117c0.372,0.18,0.714,0.465,1.026,0.855
|
||||
c0.312,0.39,0.468,0.933,0.468,1.629c0,0.72-0.181,1.263-0.54,1.629s-0.756,0.627-1.188,0.783
|
||||
c-0.373,0.132-0.718,0.21-1.035,0.233c-0.318,0.024-0.531,0.036-0.639,0.036h-6.75v-10.368H116.999z M114.227,272.539h1.332
|
||||
c0.06,0,0.129-0.003,0.207-0.009s0.158-0.021,0.242-0.045c0.145-0.036,0.276-0.108,0.396-0.216c0.12-0.108,0.18-0.282,0.18-0.522
|
||||
c0-0.023,0-0.045,0-0.063c0-0.019-0.006-0.039-0.018-0.063c-0.023-0.168-0.111-0.324-0.261-0.468
|
||||
c-0.15-0.145-0.399-0.216-0.747-0.216h-2.034v5.58h2.069c0.349,0,0.631-0.081,0.847-0.243s0.324-0.405,0.324-0.729
|
||||
c0-0.264-0.063-0.465-0.188-0.603c-0.127-0.138-0.262-0.237-0.406-0.297c-0.132-0.061-0.254-0.093-0.369-0.1
|
||||
c-0.113-0.006-0.183-0.009-0.207-0.009h-1.367V272.539z"/>
|
||||
<path d="M121.931,278.911v-10.368h3.402c0.516,0.42,1.002,0.918,1.458,1.494s0.864,1.17,1.224,1.782
|
||||
c0.204,0.336,0.385,0.669,0.541,0.999c0.155,0.33,0.293,0.645,0.414,0.944h0.035c0.204-0.527,0.424-1.014,0.657-1.458
|
||||
c0.233-0.443,0.478-0.852,0.729-1.224c0.504-0.744,0.975-1.329,1.412-1.755c0.438-0.426,0.717-0.687,0.838-0.783h3.401v10.368
|
||||
h-3.401v-5.742c-0.373,0.324-0.715,0.738-1.026,1.242c-0.313,0.504-0.589,1.026-0.828,1.566c-0.132,0.312-0.249,0.62-0.351,0.927
|
||||
c-0.103,0.306-0.189,0.591-0.262,0.854H127.8c-0.061-0.239-0.136-0.495-0.226-0.765c-0.091-0.27-0.188-0.537-0.298-0.801
|
||||
c-0.252-0.601-0.545-1.173-0.881-1.719s-0.69-0.981-1.063-1.306v5.742H121.931z"/>
|
||||
<path d="M147.474,278.785c-0.517,0.108-1.045,0.191-1.584,0.252c-0.541,0.06-1.129,0.09-1.765,0.09
|
||||
c-2.028,0-3.606-0.498-4.733-1.494c-1.129-0.995-1.692-2.321-1.692-3.978c0-1.788,0.555-3.126,1.665-4.014
|
||||
c1.109-0.889,2.463-1.332,4.059-1.332c0.275,0,0.579,0.018,0.909,0.054s0.663,0.102,0.999,0.198
|
||||
c0.828,0.24,1.587,0.753,2.277,1.539c0.689,0.785,1.035,2.078,1.035,3.879c0,0.048,0,0.099,0,0.152c0,0.055,0,0.105,0,0.153
|
||||
c0,0.061-0.004,0.114-0.01,0.162s-0.009,0.102-0.009,0.162h-6.407c-0.013-0.024-0.031-0.051-0.055-0.081
|
||||
c-0.024-0.03-0.042-0.063-0.055-0.1c-0.06-0.107-0.11-0.236-0.152-0.387c-0.042-0.149-0.063-0.32-0.063-0.513
|
||||
c0-0.108,0.006-0.24,0.018-0.396c0.012-0.155,0.042-0.329,0.09-0.521h3.366c-0.024-0.54-0.153-0.93-0.388-1.17
|
||||
c-0.233-0.24-0.488-0.396-0.765-0.469c-0.132-0.023-0.258-0.041-0.378-0.054c-0.12-0.012-0.234-0.018-0.342-0.018
|
||||
c-0.816,0-1.404,0.246-1.764,0.737c-0.36,0.492-0.541,1.11-0.541,1.854c0,0.061,0,0.12,0,0.18c0,0.061,0.006,0.12,0.019,0.181
|
||||
c0.06,0.672,0.351,1.29,0.873,1.854c0.522,0.564,1.474,0.846,2.853,0.846c0.444,0,0.855-0.032,1.233-0.099
|
||||
c0.378-0.065,0.747-0.165,1.106-0.297c0.037-0.012,0.069-0.024,0.1-0.036c0.029-0.012,0.063-0.023,0.1-0.036V278.785z"/>
|
||||
<path d="M156.995,271.243c-0.036-0.012-0.088-0.033-0.153-0.063c-0.065-0.029-0.147-0.063-0.243-0.099
|
||||
c-0.144-0.048-0.309-0.09-0.494-0.126c-0.187-0.036-0.381-0.054-0.586-0.054c-0.048,0-0.093,0-0.135,0s-0.087,0.006-0.135,0.018
|
||||
c-0.54,0.048-1.032,0.276-1.477,0.685c-0.443,0.407-0.666,1.134-0.666,2.178c0,0.804,0.145,1.398,0.433,1.782
|
||||
s0.606,0.647,0.954,0.792c0.203,0.084,0.404,0.138,0.604,0.162c0.197,0.023,0.362,0.035,0.494,0.035
|
||||
c0.229,0,0.436-0.012,0.621-0.035c0.186-0.024,0.375-0.066,0.566-0.126c0.061-0.024,0.127-0.052,0.199-0.081
|
||||
c0.071-0.03,0.144-0.063,0.215-0.1v2.7c-0.144,0.036-0.278,0.065-0.404,0.09s-0.25,0.048-0.369,0.072
|
||||
c-0.156,0.023-0.314,0.039-0.477,0.045s-0.34,0.009-0.531,0.009c-2.064,0-3.549-0.537-4.455-1.61
|
||||
c-0.906-1.074-1.359-2.319-1.359-3.735c0-0.145,0.003-0.285,0.01-0.423c0.006-0.138,0.021-0.279,0.045-0.423
|
||||
c0.131-1.164,0.608-2.227,1.431-3.187c0.821-0.96,2.175-1.439,4.06-1.439c0.359,0,0.666,0.015,0.918,0.045
|
||||
c0.252,0.029,0.491,0.075,0.719,0.135c0.037,0,0.072,0.006,0.109,0.018c0.035,0.013,0.071,0.024,0.107,0.036V271.243z"/>
|
||||
<path d="M166.823,271.243h-2.988v7.668h-3.402v-7.668h-2.988v-2.7h9.379V271.243z"/>
|
||||
<path d="M177.173,278.785c-0.516,0.108-1.044,0.191-1.584,0.252c-0.54,0.06-1.129,0.09-1.764,0.09
|
||||
c-2.028,0-3.606-0.498-4.734-1.494c-1.128-0.995-1.691-2.321-1.691-3.978c0-1.788,0.555-3.126,1.664-4.014
|
||||
c1.109-0.889,2.463-1.332,4.059-1.332c0.276,0,0.58,0.018,0.91,0.054s0.662,0.102,0.998,0.198
|
||||
c0.828,0.24,1.588,0.753,2.277,1.539c0.689,0.785,1.035,2.078,1.035,3.879c0,0.048,0,0.099,0,0.152c0,0.055,0,0.105,0,0.153
|
||||
c0,0.061-0.003,0.114-0.009,0.162c-0.007,0.048-0.009,0.102-0.009,0.162h-6.408c-0.012-0.024-0.03-0.051-0.055-0.081
|
||||
c-0.023-0.03-0.041-0.063-0.054-0.1c-0.06-0.107-0.11-0.236-0.153-0.387c-0.041-0.149-0.063-0.32-0.063-0.513
|
||||
c0-0.108,0.006-0.24,0.018-0.396c0.012-0.155,0.043-0.329,0.09-0.521h3.367c-0.025-0.54-0.154-0.93-0.388-1.17
|
||||
s-0.489-0.396-0.765-0.469c-0.133-0.023-0.258-0.041-0.379-0.054c-0.119-0.012-0.233-0.018-0.342-0.018
|
||||
c-0.816,0-1.404,0.246-1.764,0.737c-0.359,0.492-0.54,1.11-0.54,1.854c0,0.061,0,0.12,0,0.18c0,0.061,0.006,0.12,0.019,0.181
|
||||
c0.06,0.672,0.351,1.29,0.873,1.854c0.521,0.564,1.473,0.846,2.853,0.846c0.444,0,0.854-0.032,1.233-0.099
|
||||
c0.377-0.065,0.746-0.165,1.106-0.297c0.036-0.012,0.069-0.024,0.099-0.036c0.03-0.012,0.063-0.023,0.1-0.036V278.785z"/>
|
||||
</g>
|
||||
<g enable-background="new ">
|
||||
<path d="M183.149,265.554l-0.595,9.379h-2.592l-0.612-9.379H183.149z M179.989,276.157c0.354-0.36,0.777-0.54,1.27-0.54
|
||||
s0.912,0.177,1.26,0.531c0.348,0.354,0.521,0.776,0.521,1.269s-0.174,0.912-0.521,1.26c-0.348,0.349-0.768,0.522-1.26,0.522
|
||||
s-0.915-0.178-1.27-0.531c-0.354-0.354-0.531-0.771-0.531-1.251C179.458,276.937,179.636,276.517,179.989,276.157z"/>
|
||||
</g>
|
||||
<v:paragraph v:horizAlign="1"></v:paragraph>
|
||||
<v:tabList></v:tabList>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 39 KiB |
18
rsconcept/frontend/public/manifest.json
Normal file
18
rsconcept/frontend/public/manifest.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"short_name": "КонцептПортал",
|
||||
"name": "Портал для работы с концептуальными схемами",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "64x64 32x32 24x24 16x16"
|
||||
},
|
||||
{
|
||||
"src": "logo.svg",
|
||||
"sizes": "512x512 256x256 64x64 32x32 24x24 16x16"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
rsconcept/frontend/public/robots.txt
Normal file
3
rsconcept/frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
42
rsconcept/frontend/src/App.tsx
Normal file
42
rsconcept/frontend/src/App.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import Navigation from './components/Navigation/Navigation';
|
||||
import RSFormsPage from './pages/RSFormsPage';
|
||||
import RSFormPage from './pages/RSFormPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import HomePage from './pages/HomePage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import RestorePasswordPage from './pages/RestorePasswordPage';
|
||||
import UserProfilePage from './pages/UserProfilePage';
|
||||
import RegisterPage from './pages/RegisterPage';
|
||||
import ManualsPage from './pages/ManualsPage';
|
||||
import Footer from './components/Footer';
|
||||
import RSFormCreatePage from './pages/RSFormCreatePage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className='antialiased bg-gray-50 dark:bg-gray-800'>
|
||||
<Navigation />
|
||||
<main className='min-h-[calc(100vh-107px)] px-2 h-fit'>
|
||||
<Routes>
|
||||
<Route path='/' element={ <HomePage/>} />
|
||||
|
||||
<Route path='login' element={ <LoginPage/>} />
|
||||
<Route path='signup' element={<RegisterPage/>} />
|
||||
<Route path='restore-password' element={ <RestorePasswordPage/>} />
|
||||
<Route path='profile' element={<UserProfilePage/>} />
|
||||
|
||||
<Route path='manuals' element={<ManualsPage/>} />
|
||||
|
||||
<Route path='rsforms' element={<RSFormsPage/>} />
|
||||
<Route path='rsforms/:id' element={ <RSFormPage/>} />
|
||||
<Route path='rsform-create' element={ <RSFormCreatePage/>} />
|
||||
<Route path='*' element={ <NotFoundPage/>} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
50
rsconcept/frontend/src/components/BackendError.tsx
Normal file
50
rsconcept/frontend/src/components/BackendError.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import axios, { AxiosError } from 'axios';
|
||||
import PrettyJson from './Common/PrettyJSON';
|
||||
|
||||
export type ErrorInfo = string | Error | AxiosError | undefined;
|
||||
|
||||
interface BackendErrorProps {
|
||||
error: ErrorInfo
|
||||
}
|
||||
|
||||
function DescribeError(error: ErrorInfo) {
|
||||
if (!error) {
|
||||
return <p>Ошибки отсутствуют</p>;
|
||||
} else if (typeof error === 'string') {
|
||||
return <p>{error}</p>;
|
||||
} else if (axios.isAxiosError(error)) {
|
||||
if (!error?.response) {
|
||||
return <p>Нет ответа от сервера</p>;
|
||||
}
|
||||
if (error.response.status === 404) {
|
||||
return (
|
||||
<div className='flex flex-col justify-start'>
|
||||
<p>{`Обращение к несуществующему API`}</p>
|
||||
<PrettyJson data={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='flex flex-col justify-start'>
|
||||
<p className='underline'>Ошибка</p>
|
||||
<p>{error.message}</p>
|
||||
{error.response.data && (<>
|
||||
<p className='mt-2 underline'>Описание</p>
|
||||
<PrettyJson data={error.response.data} />
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <PrettyJson data={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
function BackendError({error}: BackendErrorProps) {
|
||||
return (
|
||||
<div className='py-2 text-sm font-semibold text-red-600 dark:text-red-400'>
|
||||
{DescribeError(error)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BackendError;
|
33
rsconcept/frontend/src/components/Common/Button.tsx
Normal file
33
rsconcept/frontend/src/components/Common/Button.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
interface ButtonProps {
|
||||
text?: string
|
||||
icon?: React.ReactNode
|
||||
tooltip?: string
|
||||
disabled?: boolean
|
||||
dense?: boolean
|
||||
loading?: boolean
|
||||
colorClass?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function Button({text, icon, dense=false, disabled=false, tooltip, colorClass, loading, onClick}: ButtonProps) {
|
||||
const padding = dense ? 'px-1 py-1' : 'px-3 py-2 '
|
||||
const cursor = 'disabled:cursor-not-allowed ' + (loading ? 'cursor-progress ': 'cursor-pointer ')
|
||||
const baseColor = 'dark:disabled:text-gray-800 disabled:text-gray-400 bg-gray-200 hover:bg-gray-300 dark:bg-gray-500 dark:hover:bg-gray-400'
|
||||
const color = baseColor + ' ' + (colorClass || 'text-gray-600 dark:text-zinc-50')
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
title={tooltip}
|
||||
className={padding +
|
||||
'inline-flex items-center border rounded ' + cursor + color
|
||||
}
|
||||
>
|
||||
<span>{icon}</span>
|
||||
{text && <span className={'font-bold' + (icon ? ' ml-2': '')}>{text}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button;
|
16
rsconcept/frontend/src/components/Common/Card.tsx
Normal file
16
rsconcept/frontend/src/components/Common/Card.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
interface CardProps {
|
||||
title?: string
|
||||
widthClass?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Card({title, widthClass='w-fit', children}: CardProps) {
|
||||
return (
|
||||
<div className={'border shadow-md py-2 bg-gray-50 dark:bg-gray-600 px-6 ' + widthClass}>
|
||||
{ title && <h1 className='mb-2 text-xl font-bold'>{title}</h1> }
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Card;
|
39
rsconcept/frontend/src/components/Common/Checkbox.tsx
Normal file
39
rsconcept/frontend/src/components/Common/Checkbox.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import Label from './Label';
|
||||
|
||||
interface CheckboxProps {
|
||||
id: string
|
||||
label: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
widthClass?: string
|
||||
value?: any
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function Checkbox({id, required, disabled, label, widthClass='w-full', value, onChange}: CheckboxProps) {
|
||||
return (
|
||||
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass}>
|
||||
<input id={id} type='checkbox'
|
||||
className='relative cursor-pointer peer w-4 h-4 shrink-0 mt-0.5 bg-white border rounded-sm appearance-none dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500'
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Label
|
||||
text={label}
|
||||
required={required}
|
||||
htmlFor={id}
|
||||
/>
|
||||
<svg
|
||||
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
|
||||
viewBox='0 0 512 512'
|
||||
fill='currentColor'
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Checkbox;
|
20
rsconcept/frontend/src/components/Common/ConceptTab.tsx
Normal file
20
rsconcept/frontend/src/components/Common/ConceptTab.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Tab} from 'react-tabs';
|
||||
import type { TabProps } from 'react-tabs';
|
||||
|
||||
function ConceptTab({children, className, ...otherProps} : TabProps) {
|
||||
return (
|
||||
<Tab
|
||||
className={
|
||||
'px-2 py-1 text-sm text-gray-600 bg-gray-100 hover:cursor-pointer dark:text-zinc-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400'
|
||||
+ ' ' + className
|
||||
}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
ConceptTab.tabsRole = 'Tab';
|
||||
|
||||
export default ConceptTab;
|
44
rsconcept/frontend/src/components/Common/DataTableThemed.tsx
Normal file
44
rsconcept/frontend/src/components/Common/DataTableThemed.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import DataTable, { createTheme } from 'react-data-table-component';
|
||||
import { TableProps } from 'react-data-table-component';
|
||||
import { useTheme } from '../../context/ThemeContext';
|
||||
|
||||
export interface SelectionInfo<T> {
|
||||
allSelected: boolean;
|
||||
selectedCount: number;
|
||||
selectedRows: T[];
|
||||
}
|
||||
|
||||
createTheme('customDark', {
|
||||
text: {
|
||||
primary: 'rgba(228, 228, 231, 1)',
|
||||
secondary: 'rgba(228, 228, 231, 0.87)',
|
||||
disabled: 'rgba(228, 228, 231, 0.54)',
|
||||
},
|
||||
background: {
|
||||
default: '#002b36',
|
||||
},
|
||||
context: {
|
||||
background: '#cb4b16',
|
||||
text: 'rgba(228, 228, 231, 0.87)',
|
||||
},
|
||||
divider: {
|
||||
default: '#6b6b6b',
|
||||
},
|
||||
striped: {
|
||||
default: '#004859',
|
||||
text: 'rgba(228, 228, 231, 1)',
|
||||
},
|
||||
}, 'dark');
|
||||
|
||||
function DataTableThemed<T>({theme, ...props}: TableProps<T>) {
|
||||
const { darkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<DataTable<T>
|
||||
theme={ theme ? theme : darkMode ? 'customDark' : ''}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataTableThemed;
|
55
rsconcept/frontend/src/components/Common/FileInput.tsx
Normal file
55
rsconcept/frontend/src/components/Common/FileInput.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { UploadIcon } from '../Icons';
|
||||
import Button from './Button';
|
||||
import Label from './Label';
|
||||
|
||||
interface FileInputProps {
|
||||
id: string
|
||||
required?: boolean
|
||||
label: string
|
||||
acceptType?: string
|
||||
widthClass?: string
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function FileInput({id, required, label, acceptType, widthClass='w-full', onChange}: FileInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [labelText, setLabelText] = useState('Файл не выбран');
|
||||
|
||||
const handleUploadClick = () => {
|
||||
inputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if(event.target.files && event.target.files.length > 0) {
|
||||
setLabelText(event.target.files[0].name)
|
||||
} else {
|
||||
setLabelText('Файл не выбран')
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex gap-2 py-2 mt-3 items-center '+ widthClass}>
|
||||
<input id={id} type='file'
|
||||
ref={inputRef}
|
||||
required={required}
|
||||
style={{ display: 'none' }}
|
||||
accept={acceptType}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button
|
||||
text={label}
|
||||
icon={<UploadIcon/>}
|
||||
onClick={handleUploadClick}
|
||||
/>
|
||||
<Label
|
||||
text={labelText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileInput;
|
22
rsconcept/frontend/src/components/Common/Form.tsx
Normal file
22
rsconcept/frontend/src/components/Common/Form.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Card from './Card';
|
||||
|
||||
interface FormProps {
|
||||
title: string
|
||||
widthClass?: string
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Form({title, onSubmit, widthClass='max-w-xs', children}: FormProps) {
|
||||
return (
|
||||
<div className='flex flex-col items-center w-full'>
|
||||
<Card title={title} widthClass={widthClass}>
|
||||
<form onSubmit={onSubmit}>
|
||||
{children}
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user