Initial commit

This commit is contained in:
IRBorisov 2023-07-15 17:46:19 +03:00
commit c7549b1e07
158 changed files with 23611 additions and 0 deletions

62
.dockerignore Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"python.linting.flake8Enabled": true,
"python.linting.enabled": true
}

7
TODO.txt Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
POSTGRES_USER=dev-test-user
POSTGRES_PASSWORD=02BD82EE0D
POSTGRES_DB=dev-db

12
rsconcept/RunCoverage.ps1 Normal file
View 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
View 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
View 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

View File

@ -0,0 +1 @@
Импортируемые предкомпилированные пакеты (*.whl) следует класть в import\

View 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

View 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

View 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"]

View 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

View File

View 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)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RsformConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.rsform'

View 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')},
},
),
]

View File

@ -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),
]

View 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

View 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')

View File

@ -0,0 +1,5 @@
# flake8: noqa
from .t_imports import *
from .t_views import *
from .t_models import *
from .t_serializers import *

View 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": ""
}
}
}
]
}'''

View 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')

View 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))

View 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)

View 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)),
]

View 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()

View 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})

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'

View File

@ -0,0 +1 @@
from django.contrib.auth.models import User

View 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

View File

@ -0,0 +1,3 @@
# flake8: noqa
from .t_views import *
from .t_serializers import *

View 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))

View 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')

View 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()),
]

View 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

View 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 "$@"

View 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()

View File

View 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'

View 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)

View 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()

View File

@ -0,0 +1,8 @@
tzdata
django
djangorestframework
django-cors-headers
django-filter
psycopg2-binary
gunicorn
coreapi

View File

@ -0,0 +1,7 @@
tzdata
django
djangorestframework
django-cors-headers
django-filter
coverage
coreapi

View File

@ -0,0 +1,3 @@
# Dev specific
.gitignore
node_modules

23
rsconcept/frontend/.gitignore vendored Normal file
View 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*

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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

View 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>

View 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

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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