diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69c0fc0..9eae09b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,6 +62,27 @@ jobs: push: true tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}_backend:latest + + build_gateway_and_push_to_docker_hub: + name: Push gateway Docker image to DockerHub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Push to DockerHub + uses: docker/build-push-action@v4 + with: + context: ./gateway/ + push: true + tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}_gateway:latest + deploy: runs-on: ubuntu-latest needs: @@ -74,8 +95,7 @@ jobs: with: host: ${{ secrets.HOST }} username: ${{ secrets.USER }} - key: ${{ secrets.SSH_KEY }} - passphrase: ${{ secrets.SSH_PASSPHRASE }} + password: ${{ secrets.PASSWORD}} source: "docker-compose.production.yml" target: "documents_templates" - name: Executing remote ssh commands to deploy @@ -83,15 +103,15 @@ jobs: with: host: ${{ secrets.HOST }} username: ${{ secrets.USER }} - key: ${{ secrets.SSH_KEY }} - passphrase: ${{ secrets.SSH_PASSPHRASE }} + password: ${{ secrets.PASSWORD }} script: | cd documents_templates sudo docker compose -f docker-compose.production.yml pull sudo docker compose -f docker-compose.production.yml down sudo docker compose -f docker-compose.production.yml up -d sudo docker system prune -af - + + send_message: runs-on: ubuntu-latest needs: deploy diff --git a/README.md b/README.md index 305aca0..0a1cd9f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,25 @@ многих областях, включая бизнес, юриспруденцию, образование и другие. + +## Основная логика +В основе логики шаблонизатора лежит создание готовых word документов на основе шаблона. Шаблон создается по определенным правилам и хранится на сервере в виде docx файлов. В шаблонах предусмотрены возможности дополнительных обработок вводимых пользователем данных (склонения по падежам, доп форматирование и др), более подробно об этом можно глянуть в инструкции([ссылка](https://github.com/document-template-engine/demo-repository/files/13055545/_._._.docx)). На данном этабе шаблон и его поля создаются администратором. Система регистрации и авторизации построена на основе djoser, с некоторыми модификациями + +Невторизированный пользователь имеет возможность: + - просмотреть список всех доступных шаблонов базы /api/templates/ + - скачать шаблон документа /api/templates/{id}/download_draft/ + - сформировать превью документа на основе шаблона и предоставляемых данных полей и скачать его (без сохранения на сервере) /api/templates/{id}/download_preview/ + +Авторизованный пользователь имеет возможность: + - формировать документы на основании выбранного шаблона и предоставленных данных и сохранять документы на сервере + - скачивать сохраненные документы в формате docx /api/documents/{id}/download_dpcument/ + - добавлять шаблоны или документы в избранное + - доступ к документам и данным отдельных документов имеет только администратор или автор документа + +Администратор имеет возможность: + - загрузить информацию о новом шаблоне (наименование, описание полей) POST /api/templates/ + - обновить docx файл шаблона PUT(PATCH) /api/templates/{id}/upload_template/ + ### Технологии - **Python - 3.9** - **Django - 3.2** @@ -17,7 +36,6 @@ ### Авторы - [Nikki Nikonor](https://github.com/Paymir121) -- [Дубинин Николай](https://github.com/dubininnik) - [Тимченко Александр](https://github.com/ASTimch) - [Скуридин Андрей](https://github.com/andrzej-skuridin) - [Николай Петров](https://github.com/NikolayPetrow23) @@ -30,108 +48,113 @@ Клонируете репозиторий: ```bash -git clone git@github.com:document-template-engine/backend.git + git clone git@github.com:document-template-engine/backend.git ``` -### Cоздать и активировать виртуальное окружение: +### Cоздать виртуальное окружение: +``` + python -m venv venv +``` +# активировать виртуальное окружение, Если у вас Linux/macOS ``` -python -m venv venv - -# Если у вас Linux/macOS - source venv/bin/activate - -# Если у вас windows - +``` +# Активировать виртуальное окружение, Если у вас windows +``` source venv/scripts/activate - ``` + ### Установить зависимости из файла requirements.txt: ``` -cd backend -python -m pip install --upgrade pip -pip install -r requirements.txt + cd backend + python -m pip install --upgrade pip + pip install -r requirements.txt ``` ### Выполнить миграции: ``` -cd backend -python manage.py makemigrations -python manage.py migrate + python manage.py makemigrations + python manage.py migrate ``` ### Запустить проект: ``` -cd backend -python manage.py runserver + python manage.py runserver ``` ### Создать суперпользователя: ``` -cd backend -python manage.py createsuperuser + python manage.py createsuperuser ``` ### Добавить темлейтов в базу: ``` -cd backend -python manage.py init_templates + python manage.py init_field_types + python manage.py init_templates + ``` ## Запуск докер контейнеров на локальной машине: ### Билдим проект и запускаем: ``` -docker compose up --build + docker compose up --build ``` ### Выполнить миграции: ``` -docker compose exec backend python manage.py migrate + docker compose exec backend python manage.py migrate ``` ### Выполнить создание суперпользователя: ``` -docker compose exec backend python manage.py createsuperuser + docker compose exec backend python manage.py createsuperuser ``` ### Выполнить Собрать статику Django: ``` -docker compose exec backend python manage.py collectstatic -sudo docker compose -f docker-compose.production.yml exec backend cp -r /app/collected_static/. /app/static/ + sudo docker compose exec backend python manage.py collectstatic + sudo docker compose exec backend cp -r /app/collected_static/. /app/static/ ``` ## Запуск докер контейнеров на удаленной машине: ### Выполнить обновление apt: ``` -sudo apt update + sudo apt update ``` ### Билдим проект и запускаем: ``` -sudo docker compose -f docker-compose.production.yml up --build + sudo docker compose -f docker-compose.production.yml up --build ``` ### Выполнить миграции: ``` -docker compose -f docker-compose.production.yml exec backend python manage.py migrate + sudo docker compose -f docker-compose.production.yml exec backend python manage.py migrate ``` ### Выполнить миграции: ``` -docker compose -f docker-compose.production.yml exec backend python manage.py createsuperuser + docker compose -f docker-compose.production.yml exec backend python manage.py createsuperuser +``` + +### Выполнить Собрать статику Django: +``` + sudo docker compose -f docker-compose.production.yml exec backend python manage.py collectstatic + sudo docker compose -f docker-compose.production.yml exec backend cp -r /app/collected_static/. /app/static/ ``` ### Выполнить миграции: ``` -sudo docker compose -f docker-compose.production.yml exec backend python manage.py init_templates + sudo docker compose -f docker-compose.production.yml exec backend python manage.py init_field_types + sudo docker compose -f docker-compose.production.yml exec backend python manage.py init_templates ``` ### Настройки nginx: ``` -sudo nano /etc/nginx/sites-enabled/default + sudo nano /etc/nginx/sites-enabled/default ``` ## Примеры запросов и ответов к API @@ -139,7 +162,7 @@ sudo nano /etc/nginx/sites-enabled/default ### Регистрация #### Endpoint ``` -POST api/v1/users/ + POST api/v1/users/ ``` #### Пример запроса ``` @@ -159,7 +182,7 @@ POST api/v1/users/ ### Аутентификация #### Endpoint ``` -POST api/v1/auth/token/login/ + POST api/v1/auth/token/login/ ``` #### Пример запроса @@ -180,7 +203,7 @@ POST api/v1/auth/token/login/ ### Узнать свои данные #### Endpoint. ``` -GET api/v1/users/me/ + GET api/v1/users/me/ ``` #### Пример ответа @@ -194,7 +217,7 @@ GET api/v1/users/me/ ### Просмотр списка пользователей #### Endpoint ``` -GET api/v1/users/ + GET api/v1/users/ ``` #### Пример ответа. @@ -215,17 +238,3 @@ GET api/v1/users/ ] } ``` - -## Основная логика -В основе логики шаблонизатора лежит создание готовых word документов на основе шаблона. Шаблон создается по определенным правилам и хранится на сервере в виде docx файлов. В шаблонах предусмотрены возможности дополнительных обработок вводимых пользователем данных (склонения по падежам, доп форматирование и др), более подробно об этом можно глянуть в инструкции([ссылка](https://github.com/document-template-engine/demo-repository/files/13055545/_._._.docx)). На данном этабе шаблон и его поля создаются администратором. Система регистрации и авторизации построена на основе djoser, с некоторыми модификациями - -Невторизированный пользователь имеет возможность: - - просмотреть список всех доступных шаблонов базы /api/templates/ - - скачать шаблон документа /api/templates/{id}/download_draft/ - - сформировать превью документа на основе шаблона и предоставляемых данных полей и скачать его (без сохранения на сервере) /api/templates/{id}/download_preview/ -Авторизованный пользователь имеет возможность: - - формировать документы на основании выбранного шаблона и предоставленных данных и сохранять документы на сервере - - скачивать сохраненные документы в формате docx /api/documents/{id}/download_dpcument/ - - добавлять шаблоны или документы в избранное - - доступ к документам и данным отдельных документов имеет только администратор или автор документа - diff --git a/backend/Dockerfile b/backend/Dockerfile index 33c9113..c9bf194 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,22 +1,15 @@ -FROM python:3.9-bullseye +FROM python:3.9 -ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - libc6 \ - libgcc1 \ - libgssapi-krb5-2 \ - libicu67 \ - libssl1.1 \ - libstdc++6 \ - zlib1g +RUN echo "deb http://deb.debian.org/debian bookworm main contrib" | tee /etc/apt/sources.list +RUN apt-get update +RUN apt-get install -y --no-install-recommends libreoffice-writer +RUN apt-get install -y libreoffice-java-common +RUN apt-get install -y ttf-mscorefonts-installer WORKDIR /app COPY requirements.txt . - RUN pip install -r requirements.txt --no-cache-dir COPY . . - -RUN chmod a+x commands/app.sh +CMD ["gunicorn", "--bind", "0.0.0.0:9000", "backend.wsgi"] diff --git a/backend/api/urls.py b/backend/api/urls.py index b4a93e2..e0ca5df 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -5,7 +5,8 @@ from django.conf.urls import url urlpatterns = [ - path("", include("api.v1.urls")), + # path("v1/", include("api.v1.urls")), + path("v2/", include("api.v2.urls")), ] diff --git a/backend/api/v1/permissions.py b/backend/api/v1/permissions.py index fe42e00..0f795c3 100644 --- a/backend/api/v1/permissions.py +++ b/backend/api/v1/permissions.py @@ -29,9 +29,30 @@ def has_object_permission(self, request, view, obj): return obj.owner == request.user or request.user.is_superuser +class IsAdminOrReadOnly(permissions.BasePermission): + """Доступ: Админ или только для чтения""" + + def has_permission(self, request, view): + """Видеть список могут все, добавлять только администратор.""" + + return request.method in permissions.SAFE_METHODS or ( + request.user.is_authenticated and request.user.is_superuser + ) + + def has_object_permission(self, request, view, obj): + return ( + request.method in permissions.SAFE_METHODS + or request.user.is_superuser + ) + + class IsOwner(permissions.BasePermission): """Доступ: только владелец.""" + def has_permission(self, request, view): + """Видеть список может только владелец.""" + return request.user.is_authenticated + def has_object_permission(self, request, view, obj): """Под объектом подразумевается Document.""" return request.user.is_authenticated and obj.owner == request.user diff --git a/backend/api/v1/serializers.py b/backend/api/v1/serializers.py index 7d745e2..c917814 100644 --- a/backend/api/v1/serializers.py +++ b/backend/api/v1/serializers.py @@ -1,10 +1,13 @@ """Сериализаторы для API.""" -from django.core.files.base import ContentFile +import base64 from django.contrib.auth import get_user_model +from django.utils import timezone +from django.core.files.base import ContentFile from django.db import transaction from djoser.serializers import UserSerializer from rest_framework import serializers +from .utils import custom_fieldtypes_validation, get_non_unique_items from core.constants import Messages from documents.models import ( Category, @@ -14,6 +17,8 @@ FavTemplate, Template, TemplateField, + TemplateFieldGroup, + TemplateFieldType, ) User = get_user_model() @@ -57,6 +62,22 @@ class Meta: ) +class TemplateFieldWriteSerializer(serializers.ModelSerializer): + """Сериализатор поля шаблона для записи/обновления""" + + type = serializers.SlugRelatedField( + queryset=TemplateFieldType.objects.all(), slug_field="type" + ) + group = serializers.IntegerField(required=False, default=None) + default = serializers.CharField( + trim_whitespace=False, max_length=255, required=False + ) + + class Meta: + model = TemplateField + fields = ("tag", "name", "hint", "group", "type", "length", "default") + + class TemplateFieldSerializerMinified(serializers.ModelSerializer): """Сериализатор поля шаблона сокращенный (без полей группы)""" @@ -73,9 +94,18 @@ class Meta: "type", "mask", "length", + "default", ) +class TemplateGroupSerializerMinified(serializers.ModelSerializer): + """Сериализатор группы полей шаблона без вложенных полей""" + + class Meta: + model = TemplateFieldGroup + fields = ("id", "name") + + class TemplateGroupSerializer(serializers.ModelSerializer): """Сериализатор группы полей шаблона""" @@ -100,6 +130,19 @@ def to_representation(self, instance): return response +class TemplateGroupWriteSerializer(serializers.ModelSerializer): + """Сериализатор группы полей шаблона для записи/обновления""" + + id = serializers.IntegerField() + + class Meta: + model = TemplateFieldGroup + fields = ( + "id", + "name", + ) + + class TemplateSerializerMinified(serializers.ModelSerializer): """Сериализатор шаблонов сокращенный.""" @@ -139,11 +182,18 @@ class TemplateSerializerPlain(TemplateSerializerMinified): allow_empty=True, ) + groups = TemplateGroupSerializerMinified( + source="field_groups", + read_only=True, + many=True, + allow_empty=True, + ) + class Meta(TemplateSerializerMinified.Meta): model = Template exclude = ("template",) # fields = "__all__" - read_only_fields = ("is_favorited",) + read_only_fields = ("is_favorited", "groups") class TemplateSerializer(TemplateSerializerMinified): @@ -176,6 +226,79 @@ def to_representation(self, instance): return response +class TemplateWriteSerializer(serializers.ModelSerializer): + """Сериализатор шаблонов для записи/изменения.""" + + fields = TemplateFieldWriteSerializer(many=True) + groups = TemplateGroupWriteSerializer(many=True) + + class Meta: + model = Template + fields = ("name", "deleted", "description", "fields", "groups") + + def validate(self, data): + # проверка, что все поля имеют уникальные тэги + data_fields = data.get("fields") + field_tags = [f["tag"] for f in data_fields] + tags_duplicates = get_non_unique_items(field_tags) + if tags_duplicates: + raise serializers.ValidationError( + detail=Messages.TEMPLATE_FIELD_TAGS_ARE_NOT_UNIQUE.format( + tags_duplicates + ) + ) + + # проверка, что все группы имеют уникальный id + data_groups = data.get("groups") + group_ids = [g["id"] for g in data_groups] + ids_duplicates = get_non_unique_items(group_ids) + if ids_duplicates: + raise serializers.ValidationError( + detail=Messages.TEMPLATE_GROUP_IDS_ARE_NOT_UNIQUE.format( + ids_duplicates + ) + ) + + # проверка, что поля шаблона привязаны к описанным группам в group + field_groups = set([f.get("group") for f in data_fields]) + if None in field_groups: + field_groups.discard(None) + unknown_groups = field_groups - set(group_ids) + if unknown_groups: + raise serializers.ValidationError( + detail=Messages.UNKNOWN_GROUP_ID.format(unknown_groups) + ) + return data + + def create(self, data): + data_fields = data.pop("fields") + data_groups = data.pop("groups") + template = Template.objects.create(**data) + # создание групп + data_groups.sort(key=lambda x: x["id"]) + group_models = {} + for group in data_groups: + model = TemplateFieldGroup.objects.create( + name=group["name"], template=template + ) + group_models[group["id"]] = model + # создание полей + template_fields = [] + for data in data_fields: + group_id = data.get("group") + if group_id: + data["group"] = group_models[group_id] + template_fields.append(TemplateField(template=template, **data)) + TemplateField.objects.bulk_create(template_fields) + return template + + def to_representation(self, instance): + request = self.context.get("request") + return TemplateSerializerPlain( + instance, context={"request": request} + ).data + + class DocumentFieldSerializer(serializers.ModelSerializer): """Сериализатор поля документов.""" @@ -186,6 +309,24 @@ class Meta: exclude = ("document",) +class DocumentFieldWriteSerializer(serializers.ModelSerializer): + """Сериализатор для полей документа или превью шаблона.""" + + # description = serializers.CharField(required=False, max_length=200) + + class Meta: + model = DocumentField + fields = ("field", "value") + + def validate_field(self, template_field): + template_fields = self.context.get("template_fields", set()) + if template_field not in template_fields: + raise serializers.ValidationError( + Messages.WRONG_TEMPLATE_FIELD.format(template_field.id) + ) + return template_field + + class DocumentReadSerializerMinified(serializers.ModelSerializer): """Сериализатор документов сокращенный (без информации о полях)""" @@ -293,20 +434,25 @@ class Meta: @transaction.atomic def create(self, validated_data): """Создание документа и полей документа""" - document_fields = validated_data.pop("document_fields") + document_fields = validated_data.pop("document_fields", None) document = Document.objects.create(**validated_data) + custom_fieldtypes_validation(document_fields) document.create_document_fields(document_fields) return document @transaction.atomic def update(self, instance, validated_data): """Обновление документа и полей документа""" - document_fields = validated_data.pop("document_fields") - Document.objects.filter(id=instance.id).update(**validated_data) + document_fields = validated_data.pop("document_fields", None) + Document.objects.filter(id=instance.id).update( + **validated_data, updated=timezone.now() + ) document = Document.objects.get(id=instance.id) - document.document_fields.all().delete() - document.create_document_fields(document_fields) - return instance + if document_fields is not None: + custom_fieldtypes_validation(document_fields) + document.document_fields.all().delete() + document.create_document_fields(document_fields) + return document def to_representation(self, instance): return DocumentReadSerializerMinified( @@ -332,22 +478,15 @@ class Meta: fields = "__all__" -class DocumentFieldForPreviewSerializer(serializers.ModelSerializer): - """Сериализатор для полей превью документа.""" - - description = serializers.CharField(required=False, max_length=200) +class TemplateFileUploadSerializer(serializers.ModelSerializer): + errors = serializers.SerializerMethodField() class Meta: - model = DocumentField - fields = "__all__" + model = Template + fields = ("template", "errors") - def validate_field(self, template_field): - template_fields = self.context.get("template_fields", set()) - if template_field not in template_fields: - raise serializers.ValidationError( - Messages.WRONG_TEMPLATE_FIELD.format(template_field.id) - ) - return template_field + def get_errors(self, instance): + return instance.get_consistency_errors() class CustomUserSerializer(UserSerializer): diff --git a/backend/api/v1/tests.py b/backend/api/v1/tests.py new file mode 100644 index 0000000..0058ea3 --- /dev/null +++ b/backend/api/v1/tests.py @@ -0,0 +1,193 @@ +import json + +from api.v1.serializers import TemplateWriteSerializer +from django.test import TestCase + +from core.constants import Messages +from documents.models import Template, TemplateFieldType + +duplicate_fields_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "str"}, + {"tag": "tag1", "name": "Поле 3", "type": "str"}, + ], + "groups": [], +} + +duplicate_group_ids_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "str"}, + {"tag": "tag3", "name": "Поле 3", "type": "str"}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + {"id": 1, "name": "Группа 3"}, + ], +} + +unknown_group_id_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str", "group": 1}, + {"tag": "tag2", "name": "Поле 2", "type": "str", "group": 2}, + {"tag": "tag3", "name": "Поле 3", "type": "str", "group": 3}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + ], +} + +valid_template_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str", "group": 1}, + {"tag": "tag2", "name": "Поле 2", "type": "int", "group": 2}, + {"tag": "tag3", "name": "Поле 3", "type": "str", "group": 2}, + {"tag": "tag4", "name": "Поле 4", "type": "int"}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + ], +} + +unknown_field_type_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "int"}, + {"tag": "tag3", "name": "Поле 3", "type": "unknown_type"}, + ], + "groups": [], +} + +templatefieldtype_fixture = [ + {"type": "int", "name": "Целочисленный"}, + {"type": "str", "name": "Строковый"}, +] + + +class Test(TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + for data in templatefieldtype_fixture: + TemplateFieldType.objects.create(**data) + + def test_duplicate_field_tags_is_not_valid(self): + """Проверка, что дубликатные тэги полей взводят ошибку""" + + serializer = TemplateWriteSerializer(data=duplicate_fields_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.TEMPLATE_FIELD_TAGS_ARE_NOT_UNIQUE.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_duplicate_group_ids_is_not_valid(self): + """Проверка, что дубликатные id групп взводят ошибку""" + + serializer = TemplateWriteSerializer(data=duplicate_group_ids_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.TEMPLATE_GROUP_IDS_ARE_NOT_UNIQUE.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_undefined_field_group_is_not_valid(self): + """Проверка, что неописанные id групп в полях взводят ошибку""" + + serializer = TemplateWriteSerializer(data=unknown_group_id_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.UNKNOWN_GROUP_ID.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_unknown_field_type_is_not_valid(self): + """Проверка, что неописанный тип поля взводит ошибку""" + + serializer = TemplateWriteSerializer(data=unknown_field_type_fixture) + self.assertFalse(serializer.is_valid()) + + def test_valid_template_is_created(self): + """Проверка, что валидный шаблон успешно создается в базе""" + Template.objects.all().delete() + serializer = TemplateWriteSerializer(data=valid_template_fixture) + self.assertTrue(serializer.is_valid()) + serializer.save() + fields = valid_template_fixture.pop("fields") + groups = valid_template_fixture.pop("groups") + self.assertTrue( + Template.objects.filter(**valid_template_fixture).exists(), + "Валидный шаблон в базе не создан", + ) + template = Template.objects.filter(**valid_template_fixture).first() + + # проверка, что поля созданы и они привязаны к правильной группе + for field in fields: + with self.subTest(field=field): + self.assertTrue( + template.fields.filter( + name=field["name"], tag=field["tag"] + ).exists(), + "Поле {} для шаблона не создано".format(field), + ) + field_obj = template.fields.filter( + name=field["name"], tag=field["tag"] + ).first() + self.assertEqual( + field_obj.type.type, + field["type"], + "Поле {} привязано к неправильному типу".format(field), + ) + + # проверка, что созданы все группы для полей + for group in groups: + with self.subTest(group=group, template=template): + self.assertTrue( + template.field_groups.filter( + name=(group["name"]) + ).exists(), + "Группа {} для шаблона не создана".format(group), + ) + + # проверка, что поля привязаны к правильным группам + groups_dct = {} + for g in groups: + id = g.pop("id") + groups_dct[id] = g + for f in fields: + if "group" in f: + f["group"] = groups_dct[f["group"]] + + for field in fields: + field_obj = template.fields.filter( + name=field["name"], tag=field["tag"] + ).first() + with self.subTest(field=field): + if "group" in field: + self.assertEqual( + field_obj.group.name, + field["group"]["name"], + "Поле {} неправильно привязано к группе".format(field), + ) diff --git a/backend/api/v1/urls.py b/backend/api/v1/urls.py index d8861ea..65698cc 100644 --- a/backend/api/v1/urls.py +++ b/backend/api/v1/urls.py @@ -1,11 +1,13 @@ from api.v1.views import ( AnonymousDownloadPreviewAPIView, + CheckTemplateConsistencyAPIView, DocumentFieldViewSet, DocumentViewSet, TemplateFieldViewSet, TemplateViewSet, FavTemplateAPIview, FavDocumentAPIview, + UploadTemplateFileAPIView, # RegisterView, ) from django.urls import include, path, re_path @@ -24,20 +26,20 @@ router_v1.register( r"templates/(?P[0-9]+)/fields", - basename="fields", + basename="template_fields", viewset=TemplateFieldViewSet, ) router_v1.register( - prefix="documents", - basename="documents", - viewset=DocumentViewSet, + r"documents/(?P[0-9]+)/fields", + basename="document_fields", + viewset=DocumentFieldViewSet, ) router_v1.register( - r"documents/(?P[0-9]+)/fields", - basename="fields", - viewset=DocumentFieldViewSet, + prefix="documents", + basename="documents", + viewset=DocumentViewSet, ) urlpatterns = [ @@ -47,11 +49,21 @@ path( "documents//favorite/", FavDocumentAPIview.as_view() ), - re_path( - r"^templates/(?P[0-9]+)/download_preview/$", + path( + "templates//download_preview/", AnonymousDownloadPreviewAPIView.as_view(), name="download_preview", ), + path( + "templates//check_consistency/", + CheckTemplateConsistencyAPIView.as_view(), + name="check_consistency", + ), + re_path( + "templates//upload_template/", + UploadTemplateFileAPIView.as_view(), + name="upload_template", + ), # path("users/", RegisterView.as_view(), name="register"), path("", include(router_v1.urls)), path("", include("djoser.urls")), diff --git a/backend/api/v1/utils.py b/backend/api/v1/utils.py index 5f152d0..beeb1cf 100644 --- a/backend/api/v1/utils.py +++ b/backend/api/v1/utils.py @@ -1,5 +1,19 @@ +"""Утилиты.""" + +import datetime +import io +import logging +import pathlib +import subprocess +import tempfile +from typing import Any, Dict, List, Set, Union + from django.core.mail import send_mail +from documents.models import Document, DocumentTemplate, TemplateField + +logger = logging.getLogger(__name__) + class Util: @staticmethod @@ -11,3 +25,86 @@ def send_email(data): [data["to_email"]], fail_silently=False, ) + + +def get_non_unique_items(items: List[Any]) -> Set[Any]: + """Возвращает множество неуникальных элементов списка.""" + visited = set() + non_unique = set() + for item in items: + if item not in visited: + visited.add(item) + else: + non_unique.add(item) + return non_unique + + +def fill_docx_template_for_document(document: Document) -> io.BytesIO: + """Создание документа из шаблона.""" + context = { + docfield.field.tag: docfield.value + for docfield in document.document_fields.all() + } + context_default = { + field.tag: field.default or field.name + for field in document.template.fields.all() + } + path = document.template.template + doc = DocumentTemplate(path) + buffer = doc.get_partial(context, context_default) + return buffer + + +def create_document_pdf_for_export(document: Document) -> io.BytesIO: + """Создание pdf-файла.""" + doc_file = fill_docx_template_for_document(document) + buffer = convert_file_to_pdf(doc_file) + return buffer + + +def convert_file_to_pdf(in_file: io.BytesIO) -> io.BytesIO: + """Файл в виде строки байт преобразуем в строку байт pdf-файла.""" + with tempfile.NamedTemporaryFile() as output: + out_file = pathlib.Path(output.name).resolve() + out_file.write_bytes(in_file.getvalue()) + subprocess.run( + [ + "soffice", + "--headless", + "--invisible", + "--nologo", + "--convert-to", + "pdf", + "--outdir", + out_file.parent, + out_file.absolute(), + ], + check=True, + ) + pdf_file = out_file.with_suffix(".pdf") + out_buffer = io.BytesIO() + out_buffer.write(pdf_file.read_bytes()) + out_buffer.seek(0) + pdf_file.unlink(missing_ok=True) + return out_buffer + + +def date_iso_to_ddmmyyyy(value: str): + """Преобразует строку из ISO формата в dd.mm.yyyy""" + try: + date = datetime.date.fromisoformat(value) + return date.strftime("%d.%m.%Y") + except Exception: + # print(e) # TODO logging + logger.debug("Invalid date:", exc_info=True) + return value + + +def custom_fieldtypes_validation( + validated_data: List[Dict[str, Union[TemplateField, str]]] +): + """Валидация полей согласно кастомным типам""" + for data in validated_data: + field = data["field"] + if field.type.type == "date": + data["value"] = date_iso_to_ddmmyyyy(data["value"]) diff --git a/backend/api/v1/views.py b/backend/api/v1/views.py index 32aaee2..31c0e82 100644 --- a/backend/api/v1/views.py +++ b/backend/api/v1/views.py @@ -1,8 +1,12 @@ """Вьюсеты v1 API.""" +from datetime import datetime import io +import logging import os +from pathlib import Path + +# import aspose.words as aw -import aspose.words as aw from django.contrib.auth import get_user_model from django.http import FileResponse from django.shortcuts import get_object_or_404 @@ -17,26 +21,31 @@ ) from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework_simplejwt.tokens import RefreshToken -from api.v1.permissions import IsOwner, IsOwnerOrAdminOrReadOnly -from api.v1.serializers import ( +from .permissions import IsAdminOrReadOnly, IsOwner, IsOwnerOrAdminOrReadOnly +from .serializers import ( CategorySerializer, - DocumentFieldForPreviewSerializer, DocumentFieldSerializer, - DocumentReadSerializerMinified, + DocumentFieldWriteSerializer, DocumentReadSerializerExtended, + DocumentReadSerializerMinified, DocumentWriteSerializer, FavDocumentSerializer, FavTemplateSerializer, TemplateFieldSerializer, + TemplateFileUploadSerializer, TemplateSerializer, TemplateSerializerMinified, + TemplateWriteSerializer, + CustomUserSerializer, ) -# from api.v1.utils import Util + +from api.v1 import utils as v1utils from core.constants import Messages from core.template_render import DocumentTemplate from documents.models import ( @@ -47,20 +56,22 @@ Template, ) +logger = logging.getLogger(__name__) + User = get_user_model() class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer - permissions_classes = (AllowAny,) + permission_classes = (AllowAny,) -def send_file(filestream, filename: str): +def send_file(filestream, filename: str, as_attachment: bool = True): """Функция подготовки открытого двоичного файла к отправке.""" response = FileResponse( streaming_content=filestream, - as_attachment=True, + as_attachment=as_attachment, filename=filename, ) return response @@ -70,8 +81,8 @@ class TemplateViewSet(viewsets.ModelViewSet): """Шаблон.""" serializer_class = TemplateSerializer - http_method_names = ("get", "delete") - permissions_classes = (AllowAny,) + http_method_names = ("get", "delete", "post") + permission_classes = (IsAdminOrReadOnly,) # AllowAny filter_backends = ( DjangoFilterBackend, filters.SearchFilter, @@ -90,6 +101,8 @@ class TemplateViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.action == "list": return TemplateSerializerMinified + elif self.action == "create": + return TemplateWriteSerializer return TemplateSerializer def get_queryset(self): @@ -106,6 +119,7 @@ def get_queryset(self): url_name="download_draft", ) def download_draft(self, request, pk=None): + # template = get_object_or_404(Template, pk=pk) template = serializers.PrimaryKeyRelatedField( many=False, queryset=Template.objects.all() ).to_internal_value(data=pk) @@ -114,6 +128,9 @@ def download_draft(self, request, pk=None): doc = DocumentTemplate(path) buffer = doc.get_draft(context) filename = f"{template.name}_шаблон.docx" + if request.query_params.get("pdf"): + buffer = v1utils.convert_file_to_pdf(buffer) + filename = f"{template.name}_шаблон.pdf" response = send_file(buffer, filename) return response @@ -137,7 +154,8 @@ class TemplateFieldViewSet(viewsets.ModelViewSet): serializer_class = TemplateFieldSerializer http_method_names = ("get",) - permissions_classes = (AllowAny,) + permission_classes = (IsAdminOrReadOnly,) + # permission_classes = (AllowAny,) # Заглушка pagination_class = None def get_queryset(self): @@ -152,7 +170,8 @@ class DocumentViewSet(viewsets.ModelViewSet): queryset = Document.objects.all() serializer_class = DocumentReadSerializerMinified http_method_names = ("get", "post", "patch", "delete") - permissions_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка filter_backends = ( filters.SearchFilter, filters.OrderingFilter, @@ -164,8 +183,12 @@ class DocumentViewSet(viewsets.ModelViewSet): def get_queryset(self): """Выдаем только список документов текущего пользователя.""" + # ЗАглушка if self.request.user.is_authenticated: return self.request.user.documents + else: + user = User.objects.get(id=1) + return Document.objects.filter(owner=user) return Document.objects.none() def get_serializer_class(self): @@ -183,6 +206,7 @@ def perform_create(self, serializer): detail=False, permission_classes=[ IsAuthenticated, + # AllowAny, # Заглушка ], url_path=r"draft", ) @@ -206,48 +230,48 @@ def history_documents(self, request): """Возвращает список законченных документов/история""" user = self.request.user queryset = Document.objects.filter(completed=True, owner=user) - serializer = DocumentReadSerializer( + serializer = DocumentReadSerializerMinified( queryset, many=True, context={"request": request} ) return Response(serializer.data, status=status.HTTP_200_OK) @action( detail=True, - permission_classes=[IsAuthenticated], + permission_classes=[ + IsAuthenticated + ], url_path=r"download_document", ) def download_document(self, request, pk=None): - """Скачивание готового документа""" - + """Скачивание готового документа.""" + logger.debug(f"Start docx generation for document_id {pk}") + start_time = datetime.utcnow() document = get_object_or_404(Document, id=pk) - context = dict() - for docfield in document.document_fields.all(): - template_field = docfield.field - context[template_field.tag] = docfield.value - context_default = { - field.tag: field.name for field in document.template.fields.all() - } - - path = document.template.template - doc = DocumentTemplate(path) - buffer = doc.get_partial(context, context_default) + buffer = v1utils.fill_docx_template_for_document(document) + docx_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for document_id {pk} is {docx_time-start_time}" + ) response = send_file(buffer, f"{document.template.name}.docx") return response @action( detail=True, - permission_classes=[IsOwner], + permission_classes=[ + IsAuthenticated + ], + url_path="download_pdf", ) def download_pdf(self, request, pk=None): - """Скачивание pdf-файла.""" - document = get_object_or_404(Document, id=pk, owner=request.user) - docx_stream = io.BytesIO( - b"".join(self.download_document(request, pk).streaming_content) + """Генерация и выдача на скачивание pdf-файла.""" + document = get_object_or_404(Document, pk=pk) + logger.debug(f"Start docx generation for document_id {pk}") + start_time = datetime.utcnow() + buffer = v1utils.create_document_pdf_for_export(document) + pdf_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for document_id {pk} is {pdf_time-start_time}" ) - docx_file = aw.Document(docx_stream) - buffer = io.BytesIO() - docx_file.save(buffer, aw.SaveFormat.PDF) - buffer.seek(0, os.SEEK_SET) response = send_file(buffer, f"{document.template.name}.pdf") return response @@ -255,24 +279,27 @@ def download_pdf(self, request, pk=None): class DocumentFieldViewSet(viewsets.ModelViewSet): """Поле шаблона.""" + queryset = Document.objects.all() serializer_class = DocumentFieldSerializer http_method_names = ("get",) - permissions_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка pagination_class = None def get_queryset(self): document_id = self.kwargs.get("document_id") document = get_object_or_404(Document, id=document_id) if ( - not (self.request.user.is_authenticated) - or document.owner != self.request.user + not self.request.user.is_staff + and document.owner != self.request.user ): raise PermissionDenied() - return document.document_fields.objects.all() + return document.document_fields.all() class FavTemplateAPIview(APIView): permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка def post(self, request, **kwargs): data = { @@ -306,6 +333,7 @@ def delete(self, request, **kwargs): class FavDocumentAPIview(APIView): permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка def post(self, request, **kwargs): data = { @@ -341,51 +369,116 @@ class AnonymousDownloadPreviewAPIView(views.APIView): permission_classes = (AllowAny,) def post(self, request, template_id): + logger.debug(f"Start preview generation for template {template_id}") + start_time = datetime.utcnow() template = get_object_or_404(Template, id=template_id) document_fields = request.data.get("document_fields") - serializer = DocumentFieldForPreviewSerializer( + serializer = DocumentFieldWriteSerializer( data=document_fields, context={"template_fields": set(template.fields.all())}, many=True, ) serializer.is_valid(raise_exception=True) + v1utils.custom_fieldtypes_validation(serializer.validated_data) context = {} for data in serializer.validated_data: if data["value"]: # write only fields with non empty value context[data["field"].tag] = data["value"] context_default = { - field.tag: field.name for field in template.fields.all() + field.tag: field.default or field.name + for field in template.fields.all() } doc = DocumentTemplate(template.template) buffer = doc.get_partial(context, context_default) - response = send_file(buffer, f"{template.name}_preview.docx") + filename = f"{template.name}_preview.docx" + docx_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for template {template_id} is {docx_time-start_time}" + ) + if request.query_params.get("pdf"): + buffer = v1utils.convert_file_to_pdf(buffer) + filename = f"{template.name}_preview.pdf" + pdf_time = datetime.utcnow() + logger.debug( + f"Time of pdf generation for template {template_id} is {pdf_time-docx_time}" + ) + response = send_file(buffer, filename) return response -# class RegisterView(generics.GenericAPIView): -# serializer_class = CustomUserSerializer - -# def post(self, request): -# user = request.data -# serializer = self.serializer_class(data=user) -# serializer.is_valid(raise_exception=True) -# serializer.save() -# user_data = serializer.data -# user = User.objects.get(email=user_data["email"]) -# token = RefreshToken.for_user(user).access_token - -# absurl = "https://documents-template.site/" + "?token=" + str(token) -# email_body = ( -# "Hi " -# + user.username -# + " Use the link below to verify your email \n" -# + absurl -# ) -# data = { -# "email_body": email_body, -# "to_email": user.email, -# "email_subject": "Verify your email", -# } - -# Util.send_email(data) -# return Response(user_data, status=status.HTTP_201_CREATED) +class CheckTemplateConsistencyAPIView(views.APIView): + permission_classes = (IsAdminUser,) + # permission_classes = (AllowAny,) # Заглушка + + def get(self, request, template_id): + template = get_object_or_404(Template, id=template_id) + errors = template.get_consistency_errors() + if errors: + return Response(data={"errors": errors}, status=status.HTTP_200_OK) + else: + return Response( + data={"result": Messages.TEMPLATE_CONSISTENT}, + status=status.HTTP_200_OK, + ) + + +class UploadTemplateFileAPIView(generics.UpdateAPIView): + queryset = Template.objects.all() + serializer_class = TemplateFileUploadSerializer + lookup_field = "id" + lookup_url_kwarg = "template_id" + permission_classes = (IsAdminUser,) + # permission_classes = (AllowAny,) # Заглушка + http_method_names = ["patch", "put"] + +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes +from django.urls import reverse_lazy +from django.views.generic import CreateView, View, TemplateView + +class RegisterView(generics.GenericAPIView): + serializer_class = CustomUserSerializer + + def post(self, request): + user = request.data + serializer = self.serializer_class(data=user) + serializer.is_valid(raise_exception=True) + serializer.save() + user_data = serializer.data + user = User.objects.get(email=user_data["email"]) + token = RefreshToken.for_user(user).access_token + + uid = urlsafe_base64_encode(force_bytes(user.pk)) + activation_url = reverse_lazy('confirm_email', kwargs={'uidb64': uid, 'token': token}) + print(activation_url) + absurl = f'http://127.0.0.1:9000/{activation_url}' + email_body = ( + "Hi " + + user.username + + " Use the link below to verify your email \n" + + absurl + ) + print(email_body) + data = { + "email_body": email_body, + "to_email": user.email, + "email_subject": "Verify your email", + } + + Util.send_email(data) + return Response(user_data, status=status.HTTP_201_CREATED) + +from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth import login +class UserConfirmEmailView(View): + def get(self, request, uidb64, token): + try: + uid = urlsafe_base64_decode(uidb64) + user = User.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + + if user is not None and default_token_generator.check_token(user, token): + user.is_active = True + user.save() + login(request, user) diff --git a/backend/api/v2/__init__.py b/backend/api/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/apps.py b/backend/api/v2/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/backend/api/v2/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/backend/api/v2/documents/__init__.py b/backend/api/v2/documents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/documents/serializers.py b/backend/api/v2/documents/serializers.py new file mode 100644 index 0000000..17ef39a --- /dev/null +++ b/backend/api/v2/documents/serializers.py @@ -0,0 +1,180 @@ +"""Сериализаторы для API.""" +import base64 +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.core.files.base import ContentFile +from django.db import transaction +from rest_framework import serializers + +from api.v2.utils import custom_fieldtypes_validation +from api.v2.templates.serializers import (TemplateGroupSerializer, + TemplateSerializerMinified, + TemplateFieldSerializerMinified) +from core.constants import Messages +from documents.models import ( + Document, + DocumentField, + FavDocument) + +User = get_user_model() + + +class DocumentFieldSerializer(serializers.ModelSerializer): + """Сериализатор поля документов.""" + + description = serializers.CharField(required=False, max_length=200) + + class Meta: + model = DocumentField + exclude = ("document",) + + +class DocumentFieldWriteSerializer(serializers.ModelSerializer): + """Сериализатор для полей документа или превью шаблона.""" + + # description = serializers.CharField(required=False, max_length=200) + + class Meta: + model = DocumentField + fields = ("field", "value") + + def validate_field(self, template_field): + template_fields = self.context.get("template_fields", set()) + if template_field not in template_fields: + raise serializers.ValidationError( + Messages.WRONG_TEMPLATE_FIELD.format(template_field.id) + ) + return template_field + + +class DocumentReadSerializerMinified(serializers.ModelSerializer): + """Сериализатор документов сокращенный (без информации о полях)""" + + is_favorited = serializers.SerializerMethodField() + + class Meta: + model = Document + fields = ( + "id", + "created", + "updated", + "completed", + "description", + "template", + "owner", + "is_favorited", + ) + + def get_is_favorited(self, document: Document) -> bool: + user = self.context.get("request").user + if not user.is_authenticated: + return False + return FavDocument.objects.filter( + user=user, document=document + ).exists() + + +class DocumentReadSerializerExtended(serializers.ModelSerializer): + """Сериализатор документов расширенный (с информацией полей шаблона).""" + + grouped_fields = TemplateGroupSerializer( + read_only=True, + many=True, + source="template.field_groups", + allow_empty=True, + ) + ungrouped_fields = serializers.SerializerMethodField() + is_favorited = serializers.SerializerMethodField() + template = TemplateSerializerMinified(read_only=True) + + class Meta: + model = Document + fields = ( + "id", + "created", + "updated", + "completed", + "description", + "template", + "owner", + "is_favorited", + "grouped_fields", + "ungrouped_fields", + ) + + def get_is_favorited(self, document: Document) -> bool: + user = self.context.get("request").user + if not user.is_authenticated: + return False + return FavDocument.objects.filter( + user=user, document=document + ).exists() + + def get_ungrouped_fields(self, instance): + solo_fields = instance.template.fields.filter(group=None).order_by( + "id" + ) + return TemplateFieldSerializerMinified(solo_fields, many=True).data + + def to_representation(self, instance): + response = super().to_representation(instance) + response["grouped_fields"].sort(key=lambda x: x["id"]) + # add field values + field_vals = {} + for document_field in instance.document_fields.all(): + field_vals[document_field.field.id] = document_field.value + for group in response["grouped_fields"]: + for field in group["fields"]: + id = field.get("id") + if id in field_vals: + field["value"] = field_vals[id] + for field in response["ungrouped_fields"]: + id = field.get("id") + if id in field_vals: + field["value"] = field_vals[id] + return response + + +class DocumentWriteSerializer(serializers.ModelSerializer): + """Сериализатор документов.""" + + document_fields = DocumentFieldSerializer(many=True) + + class Meta: + model = Document + fields = ( + "id", + "created", + "completed", + "description", + "template", + "document_fields", + ) + + @transaction.atomic + def create(self, validated_data): + """Создание документа и полей документа""" + document_fields = validated_data.pop("document_fields", None) + document = Document.objects.create(**validated_data) + custom_fieldtypes_validation(document_fields) + document.create_document_fields(document_fields) + return document + + @transaction.atomic + def update(self, instance, validated_data): + """Обновление документа и полей документа""" + document_fields = validated_data.pop("document_fields", None) + Document.objects.filter(id=instance.id).update( + **validated_data, updated=timezone.now() + ) + document = Document.objects.get(id=instance.id) + if document_fields is not None: + custom_fieldtypes_validation(document_fields) + document.document_fields.all().delete() + document.create_document_fields(document_fields) + return document + + def to_representation(self, instance): + return DocumentReadSerializerMinified( + instance, context={"request": self.context.get("request")} + ).data diff --git a/backend/api/v2/documents/views.py b/backend/api/v2/documents/views.py new file mode 100644 index 0000000..e5046cf --- /dev/null +++ b/backend/api/v2/documents/views.py @@ -0,0 +1,218 @@ +"""Вьюсеты v1 API.""" +from datetime import datetime +import logging + +from django.contrib.auth import get_user_model +from django.http import FileResponse +from django.shortcuts import get_object_or_404 +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import ( + filters, + status, + viewsets, + views, +) +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response + +from core.template_render import DocumentTemplate +from .serializers import ( + DocumentFieldSerializer, + DocumentReadSerializerExtended, + DocumentReadSerializerMinified, + DocumentWriteSerializer, + DocumentFieldWriteSerializer, +) +from api.v2 import utils as v1utils +from documents.models import Document, Template + +logger = logging.getLogger(__name__) + +User = get_user_model() + + +def send_file(filestream, filename: str, as_attachment: bool = True): + """Функция подготовки открытого двоичного файла к отправке.""" + response = FileResponse( + streaming_content=filestream, + as_attachment=as_attachment, + filename=filename, + ) + return response + + +class DocumentViewSet(viewsets.ModelViewSet): + """Документ.""" + + queryset = Document.objects.all() + serializer_class = DocumentReadSerializerMinified + http_method_names = ("get", "post", "patch", "delete") + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка + filter_backends = ( + filters.SearchFilter, + filters.OrderingFilter, + DjangoFilterBackend, + ) + pagination_class = None + filterset_fields = ("owner",) + search_fields = ("owner",) + + def get_queryset(self): + """Выдаем только список документов текущего пользователя.""" + # ЗАглушка + if self.request.user.is_authenticated: + return self.request.user.documents + else: + user = User.objects.get(id=1) + return Document.objects.filter(owner=user) + return Document.objects.none() + + def get_serializer_class(self): + """Выбор сериализатора.""" + if self.action == "retrieve": + return DocumentReadSerializerExtended + elif self.action == "list": + return DocumentReadSerializerMinified + return DocumentWriteSerializer + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + @action( + detail=False, + permission_classes=[ + IsAuthenticated, + # AllowAny, # Заглушка + ], + url_path=r"draft", + ) + def draft_documents(self, request): + """Возвращает список незаконченных документов/черновиков""" + user = self.request.user + queryset = Document.objects.filter(completed=False, owner=user) + serializer = DocumentReadSerializerMinified( + queryset, many=True, context={"request": request} + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + @action( + detail=False, + permission_classes=[ + IsAuthenticated, + ], + url_path=r"history", + ) + def history_documents(self, request): + """Возвращает список законченных документов/история""" + user = self.request.user + queryset = Document.objects.filter(completed=True, owner=user) + serializer = DocumentReadSerializerMinified( + queryset, many=True, context={"request": request} + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + @action( + detail=True, + permission_classes=[ + IsAuthenticated + ], + url_path=r"download_document", + ) + def download_document(self, request, pk=None): + """Скачивание готового документа.""" + logger.debug(f"Start docx generation for document_id {pk}") + start_time = datetime.utcnow() + document = get_object_or_404(Document, id=pk) + buffer = v1utils.fill_docx_template_for_document(document) + docx_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for document_id {pk} is {docx_time-start_time}" + ) + response = send_file(buffer, f"{document.template.name}.docx") + return response + + @action( + detail=True, + permission_classes=[ + IsAuthenticated + ], + url_path="download_pdf", + ) + def download_pdf(self, request, pk=None): + """Генерация и выдача на скачивание pdf-файла.""" + document = get_object_or_404(Document, pk=pk) + logger.debug(f"Start docx generation for document_id {pk}") + start_time = datetime.utcnow() + buffer = v1utils.create_document_pdf_for_export(document) + pdf_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for document_id {pk} is {pdf_time-start_time}" + ) + response = send_file(buffer, f"{document.template.name}.pdf") + return response + + +class DocumentFieldViewSet(viewsets.ModelViewSet): + """Поле шаблона.""" + + queryset = Document.objects.all() + serializer_class = DocumentFieldSerializer + http_method_names = ("get",) + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка + pagination_class = None + + def get_queryset(self): + document_id = self.kwargs.get("document_id") + document = get_object_or_404(Document, id=document_id) + if ( + not self.request.user.is_staff + and document.owner != self.request.user + ): + raise PermissionDenied() + return document.document_fields.all() + + +class AnonymousDownloadPreviewAPIView(views.APIView): + permission_classes = (AllowAny,) + + def post(self, request, template_id): + logger.debug(f"Start preview generation for template {template_id}") + start_time = datetime.utcnow() + template = get_object_or_404(Template, id=template_id) + document_fields = request.data.get("document_fields") + serializer = DocumentFieldWriteSerializer( + data=document_fields, + context={"template_fields": set(template.fields.all())}, + many=True, + ) + serializer.is_valid(raise_exception=True) + v1utils.custom_fieldtypes_validation(serializer.validated_data) + context = {} + for data in serializer.validated_data: + if data["value"]: # write only fields with non empty value + context[data["field"].tag] = data["value"] + context_default = { + field.tag: field.default or field.name + for field in template.fields.all() + } + doc = DocumentTemplate(template.template) + buffer = doc.get_partial(context, context_default) + filename = f"{template.name}_preview.docx" + docx_time = datetime.utcnow() + logger.debug( + f"Time of docx generation for template {template_id} is {docx_time-start_time}" + ) + if request.query_params.get("pdf"): + buffer = v1utils.convert_file_to_pdf(buffer) + filename = f"{template.name}_preview.pdf" + pdf_time = datetime.utcnow() + logger.debug( + f"Time of pdf generation for template {template_id} is {pdf_time-docx_time}" + ) + response = send_file(buffer, filename) + return response + diff --git a/backend/api/v2/favorites/__init__.py b/backend/api/v2/favorites/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/favorites/serializers.py b/backend/api/v2/favorites/serializers.py new file mode 100644 index 0000000..a0f7527 --- /dev/null +++ b/backend/api/v2/favorites/serializers.py @@ -0,0 +1,22 @@ +"""Сериализаторы для API.""" +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from documents.models import ( + FavDocument, + FavTemplate, +) + +User = get_user_model() + + +class FavTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = FavTemplate + fields = "__all__" + + +class FavDocumentSerializer(serializers.ModelSerializer): + class Meta: + model = FavDocument + fields = "__all__" diff --git a/backend/api/v2/favorites/views.py b/backend/api/v2/favorites/views.py new file mode 100644 index 0000000..14f3a50 --- /dev/null +++ b/backend/api/v2/favorites/views.py @@ -0,0 +1,93 @@ +"""Вьюсеты v1 API.""" +import logging + +from django.contrib.auth import get_user_model +from rest_framework import ( + serializers, + status, +) + +from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from .serializers import ( + FavDocumentSerializer, + FavTemplateSerializer, +) +from documents.models import ( + FavDocument, + FavTemplate, +) + +logger = logging.getLogger(__name__) + +User = get_user_model() + + +class FavTemplateAPIview(APIView): + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка + + def post(self, request, **kwargs): + data = { + "user": self.request.user.pk, + "template": self.kwargs.get("template_id"), + } + serializer = FavTemplateSerializer(data=data) + queryset = FavTemplate.objects.filter( + user=self.request.user.pk, template=self.kwargs.get("template_id") + ) + # проверка, что такого FavTemplate нет в БД + if queryset.exists(): + raise serializers.ValidationError( + "Этот шаблон уже есть в Избранном!" + ) + if serializer.is_valid(): + serializer.save() + return Response(status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, **kwargs): + queryset = FavTemplate.objects.filter( + user=self.request.user.pk, template=self.kwargs.get("template_id") + ) + # проверка, что такой FavTemplate существует в БД + if not queryset.exists(): + return Response(status=status.HTTP_404_NOT_FOUND) + queryset.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class FavDocumentAPIview(APIView): + permission_classes = (IsAuthenticated,) + # permission_classes = (AllowAny,) # Заглушка + + def post(self, request, **kwargs): + data = { + "user": self.request.user.pk, + "document": self.kwargs.get("document_id"), + } + serializer = FavDocumentSerializer(data=data) + queryset = FavDocument.objects.filter( + user=self.request.user.pk, document=self.kwargs.get("document_id") + ) + # проверка, что такого FavDocument нет в БД + if queryset.exists(): + raise serializers.ValidationError( + "Этот документ уже есть в Избранном!" + ) + if serializer.is_valid(): + serializer.save() + return Response(status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, **kwargs): + queryset = FavDocument.objects.filter( + user=self.request.user.pk, document=self.kwargs.get("document_id") + ) + # проверка, что такой FavDocument существует в БД + if not queryset.exists(): + return Response(status=status.HTTP_404_NOT_FOUND) + queryset.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/api/v2/filters.py b/backend/api/v2/filters.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/migrations/__init__.py b/backend/api/v2/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/permissions.py b/backend/api/v2/permissions.py new file mode 100644 index 0000000..0f795c3 --- /dev/null +++ b/backend/api/v2/permissions.py @@ -0,0 +1,58 @@ +"""Разрешения для API.""" + +from rest_framework import permissions + + +class IsSuperUserOrReadOnly(permissions.BasePermission): + """Доступ: Администратор или только просмотр.""" + + def has_permission(self, request, view): + """Выдача прав на уровне списка.""" + return ( + request.method in permissions.SAFE_METHODS + or request.user.is_superuser + ) + + +class IsOwnerOrAdminOrReadOnly(permissions.BasePermission): + """Доступ: Владелец/админ или только для чтения""" + + def has_permission(self, request, view): + """Видеть список могут все, добавлять только авторизованные.""" + return ( + request.method in permissions.SAFE_METHODS + or request.user.is_authenticated + ) + + def has_object_permission(self, request, view, obj): + """Под объектом подразумевается Template или Document.""" + return obj.owner == request.user or request.user.is_superuser + + +class IsAdminOrReadOnly(permissions.BasePermission): + """Доступ: Админ или только для чтения""" + + def has_permission(self, request, view): + """Видеть список могут все, добавлять только администратор.""" + + return request.method in permissions.SAFE_METHODS or ( + request.user.is_authenticated and request.user.is_superuser + ) + + def has_object_permission(self, request, view, obj): + return ( + request.method in permissions.SAFE_METHODS + or request.user.is_superuser + ) + + +class IsOwner(permissions.BasePermission): + """Доступ: только владелец.""" + + def has_permission(self, request, view): + """Видеть список может только владелец.""" + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + """Под объектом подразумевается Document.""" + return request.user.is_authenticated and obj.owner == request.user diff --git a/backend/api/v2/templates/__init__.py b/backend/api/v2/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/templates/serializers.py b/backend/api/v2/templates/serializers.py new file mode 100644 index 0000000..339fc4f --- /dev/null +++ b/backend/api/v2/templates/serializers.py @@ -0,0 +1,310 @@ +"""Сериализаторы для API.""" +import base64 +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.core.files.base import ContentFile +from django.db import transaction +from djoser.serializers import UserSerializer +from rest_framework import serializers + +from api.v2.utils import custom_fieldtypes_validation, get_non_unique_items +from core.constants import Messages +from documents.models import ( + Category, + Document, + DocumentField, + FavDocument, + FavTemplate, + Template, + TemplateField, + TemplateFieldGroup, + TemplateFieldType, +) + +User = get_user_model() + + +class Base64ImageField(serializers.ImageField): + def to_internal_value(self, data): + if isinstance(data, str) and data.startswith("data:image"): + format, imgstr = data.split(";base64,") + ext = format.split("/")[-1] + + data = ContentFile(base64.b64decode(imgstr), name="temp." + ext) + + return super().to_internal_value(data) + + +class TemplateFieldSerializer(serializers.ModelSerializer): + """Сериализатор поля шаблона.""" + + group_id = serializers.PrimaryKeyRelatedField( + source="group", read_only=True + ) + group_name = serializers.StringRelatedField( + source="group.name", read_only=True + ) + type = serializers.SlugRelatedField(slug_field="type", read_only=True) + mask = serializers.CharField(source="type.mask", read_only=True) + + class Meta: + model = TemplateField + fields = ( + "id", + "tag", + "name", + "hint", + "group_id", + "group_name", + "type", + "mask", + "length", + ) + + +class TemplateFieldWriteSerializer(serializers.ModelSerializer): + """Сериализатор поля шаблона для записи/обновления""" + + type = serializers.SlugRelatedField( + queryset=TemplateFieldType.objects.all(), slug_field="type" + ) + group = serializers.IntegerField(required=False, default=None) + default = serializers.CharField( + trim_whitespace=False, max_length=255, required=False + ) + + class Meta: + model = TemplateField + fields = ("tag", "name", "hint", "group", "type", "length", "default") + + +class TemplateFieldSerializerMinified(serializers.ModelSerializer): + """Сериализатор поля шаблона сокращенный (без полей группы)""" + + type = serializers.SlugRelatedField(slug_field="type", read_only=True) + mask = serializers.CharField(source="type.mask", read_only=True) + + class Meta: + model = TemplateField + fields = ( + "id", + "tag", + "name", + "hint", + "type", + "mask", + "length", + "default", + ) + + +class TemplateGroupSerializerMinified(serializers.ModelSerializer): + """Сериализатор группы полей шаблона без вложенных полей""" + + class Meta: + model = TemplateFieldGroup + fields = ("id", "name", ) + + +class TemplateGroupSerializer(serializers.ModelSerializer): + """Сериализатор группы полей шаблона""" + + fields = TemplateFieldSerializerMinified( + read_only=True, + many=True, + # source="fields", + allow_empty=True, + ) + + class Meta: + model = TemplateField + fields = ( + "id", + "name", + "fields", + ) + + def to_representation(self, instance): + response = super().to_representation(instance) + response["fields"].sort(key=lambda x: x["id"]) + return response + + +class TemplateGroupWriteSerializer(serializers.ModelSerializer): + """Сериализатор группы полей шаблона для записи/обновления""" + + id = serializers.IntegerField() + + class Meta: + model = TemplateFieldGroup + fields = ( + "id", + "name", + ) + + +class TemplateSerializerMinified(serializers.ModelSerializer): + """Сериализатор шаблонов сокращенный.""" + + is_favorited = serializers.SerializerMethodField() + image = Base64ImageField(required=True, allow_null=True) + + class Meta: + model = Template + exclude = ("template",) + read_only_fields = ( + "name", + "category", + "owner", + "image", + "modified", + "deleted", + "description", + "is_favorited", + ) + + def get_is_favorited(self, template: Template) -> bool: + user = self.context.get("request").user + if not user.is_authenticated: + return False + return FavTemplate.objects.filter( + user=user, template=template + ).exists() + + +class TemplateSerializerPlain(TemplateSerializerMinified): + """Сериализатор шаблона (без вложенности полей в группы).""" + + fields = TemplateFieldSerializer( + read_only=True, + many=True, + # source="fields", + allow_empty=True, + ) + + groups = TemplateGroupSerializerMinified( + source="field_groups", + read_only=True, + many=True, + allow_empty=True, + ) + + class Meta(TemplateSerializerMinified.Meta): + model = Template + exclude = ("template",) + # fields = "__all__" + read_only_fields = ("is_favorited", "groups") + + +class TemplateSerializer(TemplateSerializerMinified): + """Сериализатор шаблона (поля сгруппированы внутри grouped_fields).""" + + grouped_fields = TemplateGroupSerializer( + read_only=True, + many=True, + source="field_groups", + allow_empty=True, + ) + ungrouped_fields = serializers.SerializerMethodField() + + class Meta(TemplateSerializerMinified.Meta): + model = Template + exclude = ("template",) + read_only_fields = ( + "is_favorited", + "grouped_fields", + "ungrouped_fields", + ) + + def get_ungrouped_fields(self, instance): + solo_fields = instance.fields.filter(group=None).order_by("id") + return TemplateFieldSerializerMinified(solo_fields, many=True).data + + def to_representation(self, instance): + response = super().to_representation(instance) + response["grouped_fields"].sort(key=lambda x: x["id"]) + return response + + +class TemplateWriteSerializer(serializers.ModelSerializer): + """Сериализатор шаблонов для записи/изменения.""" + + fields = TemplateFieldWriteSerializer(many=True) + groups = TemplateGroupWriteSerializer(many=True) + + class Meta: + model = Template + fields = ("name", "deleted", "description", "fields", "groups") + + def validate(self, data): + # проверка, что все поля имеют уникальные тэги + data_fields = data.get("fields") + field_tags = [f["tag"] for f in data_fields] + tags_duplicates = get_non_unique_items(field_tags) + if tags_duplicates: + raise serializers.ValidationError( + detail=Messages.TEMPLATE_FIELD_TAGS_ARE_NOT_UNIQUE.format( + tags_duplicates + ) + ) + + # проверка, что все группы имеют уникальный id + data_groups = data.get("groups") + group_ids = [g["id"] for g in data_groups] + ids_duplicates = get_non_unique_items(group_ids) + if ids_duplicates: + raise serializers.ValidationError( + detail=Messages.TEMPLATE_GROUP_IDS_ARE_NOT_UNIQUE.format( + ids_duplicates + ) + ) + + # проверка, что поля шаблона привязаны к описанным группам в group + field_groups = set([f.get("group") for f in data_fields]) + if None in field_groups: + field_groups.discard(None) + unknown_groups = field_groups - set(group_ids) + if unknown_groups: + raise serializers.ValidationError( + detail=Messages.UNKNOWN_GROUP_ID.format(unknown_groups) + ) + return data + + def create(self, data): + data_fields = data.pop("fields") + data_groups = data.pop("groups") + template = Template.objects.create(**data) + # создание групп + data_groups.sort(key=lambda x: x["id"]) + group_models = {} + for group in data_groups: + model = TemplateFieldGroup.objects.create( + name=group["name"], template=template + ) + group_models[group["id"]] = model + # создание полей + template_fields = [] + for data in data_fields: + group_id = data.get("group") + if group_id: + data["group"] = group_models[group_id] + template_fields.append(TemplateField(template=template, **data)) + TemplateField.objects.bulk_create(template_fields) + return template + + def to_representation(self, instance): + request = self.context.get("request") + return TemplateSerializerPlain( + instance, context={"request": request} + ).data + + +class TemplateFileUploadSerializer(serializers.ModelSerializer): + errors = serializers.SerializerMethodField() + + class Meta: + model = Template + fields = ("template", "errors") + + def get_errors(self, instance): + return instance.get_consistency_errors() diff --git a/backend/api/v2/templates/views.py b/backend/api/v2/templates/views.py new file mode 100644 index 0000000..733f1cf --- /dev/null +++ b/backend/api/v2/templates/views.py @@ -0,0 +1,160 @@ +"""Вьюсеты v1 API.""" +import logging + +from django.contrib.auth import get_user_model +from django.http import FileResponse +from django.shortcuts import get_object_or_404 +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import ( + filters, + generics, + serializers, + status, + views, + viewsets, +) +from rest_framework.decorators import action +from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated +from rest_framework.response import Response + +from api.v2.permissions import IsAdminOrReadOnly, IsOwner, IsOwnerOrAdminOrReadOnly +from .serializers import ( + TemplateFieldSerializer, + TemplateFileUploadSerializer, + TemplateSerializer, + TemplateSerializerMinified, + TemplateWriteSerializer, +) + +from api.v2 import utils as v1utils +from core.constants import Messages +from core.template_render import DocumentTemplate +from documents.models import Template + + +logger = logging.getLogger(__name__) + +User = get_user_model() + + +def send_file(filestream, filename: str, as_attachment: bool = True): + """Функция подготовки открытого двоичного файла к отправке.""" + response = FileResponse( + streaming_content=filestream, + as_attachment=as_attachment, + filename=filename, + ) + return response + + +class TemplateViewSet(viewsets.ModelViewSet): + """Шаблон.""" + + serializer_class = TemplateSerializer + http_method_names = ("get", "delete", "post") + permission_classes = (IsAdminOrReadOnly,) # AllowAny + filter_backends = ( + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ) + pagination_class = None + filterset_fields = ( + "owner", + "category", + ) + search_fields = ( + "owner", + "category", + ) + + def get_serializer_class(self): + if self.action == "list": + return TemplateSerializerMinified + elif self.action == "create": + return TemplateWriteSerializer + return TemplateSerializer + + def get_queryset(self): + if self.request.user.is_superuser: + return Template.objects.all() + else: + return Template.objects.filter(deleted=False) + + @action( + detail=True, + methods=["get"], + permission_classes=(AllowAny,), + url_path="download_draft", + url_name="download_draft", + ) + def download_draft(self, request, pk=None): + # template = get_object_or_404(Template, pk=pk) + template = serializers.PrimaryKeyRelatedField( + many=False, queryset=Template.objects.all() + ).to_internal_value(data=pk) + context = {field.tag: field.name for field in template.fields.all()} + path = template.template + doc = DocumentTemplate(path) + buffer = doc.get_draft(context) + filename = f"{template.name}_шаблон.docx" + if request.query_params.get("pdf"): + buffer = v1utils.convert_file_to_pdf(buffer) + filename = f"{template.name}_шаблон.pdf" + response = send_file(buffer, filename) + return response + + def destroy(self, request, *args, **kwargs): + user = request.user + template = self.get_object() + if not (user == template.owner or user.is_superuser): + return Response(status=status.HTTP_404_NOT_FOUND) + if template.deleted: + return Response( + Messages.TEMPLATE_ALREADY_DELETED, + status=status.HTTP_404_NOT_FOUND, + ) + template.deleted = True + template.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class TemplateFieldViewSet(viewsets.ModelViewSet): + """Поля шаблона.""" + + serializer_class = TemplateFieldSerializer + http_method_names = ("get",) + permission_classes = (IsAdminOrReadOnly,) + # permission_classes = (AllowAny,) # Заглушка + pagination_class = None + + def get_queryset(self): + template_id = self.kwargs.get("template_id") + template = get_object_or_404(Template, id=template_id) + return template.fields.all() + + +class CheckTemplateConsistencyAPIView(views.APIView): + permission_classes = (IsAdminUser,) + # permission_classes = (AllowAny,) # Заглушка + + def get(self, request, template_id): + template = get_object_or_404(Template, id=template_id) + errors = template.get_consistency_errors() + if errors: + return Response(data={"errors": errors}, status=status.HTTP_200_OK) + else: + return Response( + data={"result": Messages.TEMPLATE_CONSISTENT}, + status=status.HTTP_200_OK, + ) + + +class UploadTemplateFileAPIView(generics.UpdateAPIView): + queryset = Template.objects.all() + serializer_class = TemplateFileUploadSerializer + lookup_field = "id" + lookup_url_kwarg = "template_id" + permission_classes = (IsAdminUser,) + # permission_classes = (AllowAny,) # Заглушка + http_method_names = ["patch", "put"] diff --git a/backend/api/v2/tests.py b/backend/api/v2/tests.py new file mode 100644 index 0000000..2090fb7 --- /dev/null +++ b/backend/api/v2/tests.py @@ -0,0 +1,193 @@ +import json + +from api.v1.serializers import TemplateWriteSerializer +from django.test import TestCase + +from core.constants import Messages +from documents.models import Template, TemplateFieldType + +duplicate_fields_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "str"}, + {"tag": "tag1", "name": "Поле 3", "type": "str"}, + ], + "groups": [], +} + +duplicate_group_ids_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "str"}, + {"tag": "tag3", "name": "Поле 3", "type": "str"}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + {"id": 1, "name": "Группа 3"}, + ], +} + +unknown_group_id_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str", "group": 1}, + {"tag": "tag2", "name": "Поле 2", "type": "str", "group": 2}, + {"tag": "tag3", "name": "Поле 3", "type": "str", "group": 3}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + ], +} + +valid_template_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str", "group": 1}, + {"tag": "tag2", "name": "Поле 2", "type": "int", "group": 2}, + {"tag": "tag3", "name": "Поле 3", "type": "str", "group": 2}, + {"tag": "tag4", "name": "Поле 4", "type": "int"}, + ], + "groups": [ + {"id": 1, "name": "Группа 1"}, + {"id": 2, "name": "Группа 2"}, + ], +} + +unknown_field_type_fixture = { + "name": "Тестовый шаблон", + "deleted": False, + "description": "Тестовый шаблон", + "fields": [ + {"tag": "tag1", "name": "Поле 1", "type": "str"}, + {"tag": "tag2", "name": "Поле 2", "type": "int"}, + {"tag": "tag3", "name": "Поле 3", "type": "unknown_type"}, + ], + "groups": [], +} + +templatefieldtype_fixture = [ + {"type": "int", "name": "Целочисленный"}, + {"type": "str", "name": "Строковый"}, +] + + +class Test(TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + for data in templatefieldtype_fixture: + TemplateFieldType.objects.create(**data) + + def test_duplicate_field_tags_is_not_valid(self): + """Проверка, что дубликатные тэги полей взводят ошибку""" + + serializer = TemplateWriteSerializer(data=duplicate_fields_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.TEMPLATE_FIELD_TAGS_ARE_NOT_UNIQUE.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_duplicate_group_ids_is_not_valid(self): + """Проверка, что дубликатные id групп взводят ошибку""" + + serializer = TemplateWriteSerializer(data=duplicate_group_ids_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.TEMPLATE_GROUP_IDS_ARE_NOT_UNIQUE.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_undefined_field_group_is_not_valid(self): + """Проверка, что неописанные id групп в полях взводят ошибку""" + + serializer = TemplateWriteSerializer(data=unknown_group_id_fixture) + self.assertFalse(serializer.is_valid()) + pattern = Messages.UNKNOWN_GROUP_ID.format(".*") + self.assertRegex( + json.dumps(serializer.errors, ensure_ascii=False), + rf".*{pattern}.*", + ) + + def test_unknown_field_type_is_not_valid(self): + """Проверка, что неописанный тип поля взводит ошибку""" + + serializer = TemplateWriteSerializer(data=unknown_field_type_fixture) + self.assertFalse(serializer.is_valid()) + + def test_valid_template_is_created(self): + """Проверка, что валидный шаблон успешно создается в базе""" + Template.objects.all().delete() + serializer = TemplateWriteSerializer(data=valid_template_fixture) + self.assertTrue(serializer.is_valid()) + serializer.save() + fields = valid_template_fixture.pop("fields") + groups = valid_template_fixture.pop("groups") + self.assertTrue( + Template.objects.filter(**valid_template_fixture).exists(), + "Валидный шаблон в базе не создан", + ) + template = Template.objects.filter(**valid_template_fixture).first() + + # проверка, что поля созданы и они привязаны к правильной группе + for field in fields: + with self.subTest(field=field): + self.assertTrue( + template.fields.filter( + name=field["name"], tag=field["tag"] + ).exists(), + "Поле {} для шаблона не создано".format(field), + ) + field_obj = template.fields.filter( + name=field["name"], tag=field["tag"] + ).first() + self.assertEqual( + field_obj.type.type, + field["type"], + "Поле {} привязано к неправильному типу".format(field), + ) + + # проверка, что созданы все группы для полей + for group in groups: + with self.subTest(group=group, template=template): + self.assertTrue( + template.field_groups.filter( + name=(group["name"]) + ).exists(), + "Группа {} для шаблона не создана".format(group), + ) + + # проверка, что поля привязаны к правильным группам + groups_dct = {} + for g in groups: + id = g.pop("id") + groups_dct[id] = g + for f in fields: + if "group" in f: + f["group"] = groups_dct[f["group"]] + + for field in fields: + field_obj = template.fields.filter( + name=field["name"], tag=field["tag"] + ).first() + with self.subTest(field=field): + if "group" in field: + self.assertEqual( + field_obj.group.name, + field["group"]["name"], + "Поле {} неправильно привязано к группе".format(field), + ) \ No newline at end of file diff --git a/backend/api/v2/urls.py b/backend/api/v2/urls.py new file mode 100644 index 0000000..a79bcc4 --- /dev/null +++ b/backend/api/v2/urls.py @@ -0,0 +1,72 @@ +from api.v2.documents.views import ( + AnonymousDownloadPreviewAPIView, + DocumentFieldViewSet, + DocumentViewSet,) +from api.v2.templates.views import ( + UploadTemplateFileAPIView, + CheckTemplateConsistencyAPIView, + TemplateFieldViewSet, + TemplateViewSet,) +from api.v2.favorites.views import ( + FavTemplateAPIview, + FavDocumentAPIview,) + +from django.urls import include, path, re_path +from rest_framework.routers import DefaultRouter + +app_name = "api" + +router_v1 = DefaultRouter() + + +router_v1.register( + prefix="templates", + basename="templates", + viewset=TemplateViewSet, +) + +router_v1.register( + r"templates/(?P[0-9]+)/fields", + basename="template_fields", + viewset=TemplateFieldViewSet, +) + +router_v1.register( + r"documents/(?P[0-9]+)/fields", + basename="document_fields", + viewset=DocumentFieldViewSet, +) + +router_v1.register( + prefix="documents", + basename="documents", + viewset=DocumentViewSet, +) + + +urlpatterns = [ + path( + "templates//favorite/", FavTemplateAPIview.as_view() + ), + path( + "documents//favorite/", FavDocumentAPIview.as_view() + ), + path( + "templates//download_preview/", + AnonymousDownloadPreviewAPIView.as_view(), + name="download_preview", + ), + path( + "templates//check_consistency/", + CheckTemplateConsistencyAPIView.as_view(), + name="check_consistency", + ), + re_path( + "templates//upload_template/", + UploadTemplateFileAPIView.as_view(), + name="upload_template", + ), + path("", include(router_v1.urls)), + path("", include("djoser.urls")), + path("auth/", include("djoser.urls.authtoken")), +] diff --git a/backend/api/v2/users/__init__.py b/backend/api/v2/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/v2/users/serializers.py b/backend/api/v2/users/serializers.py new file mode 100644 index 0000000..47d7348 --- /dev/null +++ b/backend/api/v2/users/serializers.py @@ -0,0 +1,31 @@ +"""Сериализаторы для API.""" + +from django.contrib.auth import get_user_model +from djoser.serializers import UserSerializer +from rest_framework import serializers + + +User = get_user_model() + + +class CustomUserSerializer(UserSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ("id", "email", "password") + read_only_fields = ("id",) + + def create(self, validated_data): + email = validated_data.get("email") + password = validated_data.get("password") + username = email + user = User(email=email, username=username) + user.set_password(password) + user.save() + return user + + def validate(self, data): + if User.objects.filter(email=data["email"]): + raise serializers.ValidationError("Такой email уже есть!") + return data diff --git a/backend/api/v2/users/views.py b/backend/api/v2/users/views.py new file mode 100644 index 0000000..3df409b --- /dev/null +++ b/backend/api/v2/users/views.py @@ -0,0 +1,74 @@ +"""Вьюсеты v1 API.""" + +import logging + +from django.contrib.auth import get_user_model +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes +from django.urls import reverse_lazy +from django.views.generic import View +from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth import login + +from rest_framework import ( + generics, + status, +) +from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + +from .serializers import CustomUserSerializer + + +logger = logging.getLogger(__name__) + +User = get_user_model() + + +class RegisterView(generics.GenericAPIView): + serializer_class = CustomUserSerializer + + def post(self, request): + user = request.data + serializer = self.serializer_class(data=user) + serializer.is_valid(raise_exception=True) + serializer.save() + user_data = serializer.data + user = User.objects.get(email=user_data["email"]) + token = RefreshToken.for_user(user).access_token + + uid = urlsafe_base64_encode(force_bytes(user.pk)) + activation_url = reverse_lazy('confirm_email', kwargs={'uidb64': uid, 'token': token}) + print(activation_url) + absurl = f'http://127.0.0.1:9000/{activation_url}' + email_body = ( + "Hi " + + user.username + + " Use the link below to verify your email \n" + + absurl + ) + print(email_body) + data = { + "email_body": email_body, + "to_email": user.email, + "email_subject": "Verify your email", + } + + Util.send_email(data) + return Response(user_data, status=status.HTTP_201_CREATED) + + +class UserConfirmEmailView(View): + def get(self, request, uidb64, token): + try: + uid = urlsafe_base64_decode(uidb64) + user = User.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + + if user is not None and default_token_generator.check_token(user, token): + user.is_active = True + user.save() + login(request, user) + diff --git a/backend/api/v2/utils.py b/backend/api/v2/utils.py new file mode 100644 index 0000000..beeb1cf --- /dev/null +++ b/backend/api/v2/utils.py @@ -0,0 +1,110 @@ +"""Утилиты.""" + +import datetime +import io +import logging +import pathlib +import subprocess +import tempfile +from typing import Any, Dict, List, Set, Union + +from django.core.mail import send_mail + +from documents.models import Document, DocumentTemplate, TemplateField + +logger = logging.getLogger(__name__) + + +class Util: + @staticmethod + def send_email(data): + send_mail( + data["email_subject"], + data["email_body"], + "draftnikox@rambler.ru", + [data["to_email"]], + fail_silently=False, + ) + + +def get_non_unique_items(items: List[Any]) -> Set[Any]: + """Возвращает множество неуникальных элементов списка.""" + visited = set() + non_unique = set() + for item in items: + if item not in visited: + visited.add(item) + else: + non_unique.add(item) + return non_unique + + +def fill_docx_template_for_document(document: Document) -> io.BytesIO: + """Создание документа из шаблона.""" + context = { + docfield.field.tag: docfield.value + for docfield in document.document_fields.all() + } + context_default = { + field.tag: field.default or field.name + for field in document.template.fields.all() + } + path = document.template.template + doc = DocumentTemplate(path) + buffer = doc.get_partial(context, context_default) + return buffer + + +def create_document_pdf_for_export(document: Document) -> io.BytesIO: + """Создание pdf-файла.""" + doc_file = fill_docx_template_for_document(document) + buffer = convert_file_to_pdf(doc_file) + return buffer + + +def convert_file_to_pdf(in_file: io.BytesIO) -> io.BytesIO: + """Файл в виде строки байт преобразуем в строку байт pdf-файла.""" + with tempfile.NamedTemporaryFile() as output: + out_file = pathlib.Path(output.name).resolve() + out_file.write_bytes(in_file.getvalue()) + subprocess.run( + [ + "soffice", + "--headless", + "--invisible", + "--nologo", + "--convert-to", + "pdf", + "--outdir", + out_file.parent, + out_file.absolute(), + ], + check=True, + ) + pdf_file = out_file.with_suffix(".pdf") + out_buffer = io.BytesIO() + out_buffer.write(pdf_file.read_bytes()) + out_buffer.seek(0) + pdf_file.unlink(missing_ok=True) + return out_buffer + + +def date_iso_to_ddmmyyyy(value: str): + """Преобразует строку из ISO формата в dd.mm.yyyy""" + try: + date = datetime.date.fromisoformat(value) + return date.strftime("%d.%m.%Y") + except Exception: + # print(e) # TODO logging + logger.debug("Invalid date:", exc_info=True) + return value + + +def custom_fieldtypes_validation( + validated_data: List[Dict[str, Union[TemplateField, str]]] +): + """Валидация полей согласно кастомным типам""" + for data in validated_data: + field = data["field"] + if field.type.type == "date": + data["value"] = date_iso_to_ddmmyyyy(data["value"]) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 3dbc708..45533a2 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -2,6 +2,7 @@ import os from pathlib import Path +import sentry_sdk from dotenv import load_dotenv load_dotenv() @@ -30,20 +31,32 @@ "api", "users", "documents", - "colorfield", "core", + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.apple', + 'allauth.socialaccount.providers.discord', + 'allauth.socialaccount.providers.github', + 'allauth.socialaccount.providers.google', + # 'allauth.socialaccount.providers.mailru', + 'allauth.socialaccount.providers.telegram', + 'allauth.socialaccount.providers.vk', + 'allauth.socialaccount.providers.yandex', ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", + # allauth + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = "backend.urls" @@ -59,6 +72,7 @@ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + 'django.template.context_processors.request', ], }, }, @@ -117,8 +131,8 @@ STATIC_ROOT = BASE_DIR / "collected_static" STATICFILES_DIRS = ((BASE_DIR / "static/"),) - -INITIAL_DATA_DIR = BASE_DIR / "data" +INITIAL_DATA_DIR = BASE_DIR / "data/" # for local run +# INITIAL_DATA_DIR = BASE_DIR / "static/data/" MEDIA_URL = "/media/" MEDIA_ROOT = "/app/media" @@ -144,31 +158,96 @@ DJOSER = { "LOGIN_FIELD": "email", - "USER_ACTIVATION": "optional", + # "USER_ACTIVATION": "optional", "PERMISSIONS": { "user_list": ["rest_framework.permissions.AllowAny"], "user": ["djoser.permissions.CurrentUserOrAdminOrReadOnly"], }, - "HIDE_USERS": False, + "HIDE_USERS": True, "PASSWORD_RESET_CONFIRM_URL": "#/set_password/{uid}/{token}", "SERIALIZERS": { "user_create": "api.v1.serializers.CustomUserSerializer", "user": "api.v1.serializers.CustomUserSerializer", + "current_user": "api.v1.serializers.CustomUserSerializer", }, - "SENDACTIVATIONEMAIL": True, - "ACTIVATION_URL": "#activation/{uid}/{token}", + # "ACTIVATION_URL": "#activation/{uid}/{token}", + #'SEND_ACTIVATION_EMAIL': True, } EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = "smtp.rambler.ru" +EMAIL_HOST = "smtp.gmail.com" EMAIL_PORT = 465 EMAIL_USE_SSL = True -EMAIL_HOST_USER = "draftnikox@rambler.ru" -EMAIL_HOST_PASSWORD = "456852Zx" +EMAIL_HOST_USER = "nikox12lamba@gmail.com" +EMAIL_HOST_PASSWORD = "fkzzqiydypuxrfvw" SWAGGER_SETTINGS = { "SECURITY_DEFINITIONS": { "Token": {"type": "apiKey", "name": "Authorization", "in": "header"} }, - "BASE_PATH": "https://documents-template.site/api/", + "BASE_PATH": "https://doki.pro/api/v2/", } + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "console": {"format": "%(name)-12s %(levelname)-8s %(message)s"}, + "file": { + "format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" + }, + }, + "handlers": { + "console": {"class": "logging.StreamHandler", "formatter": "console"}, + "file": { + "level": "DEBUG", + "class": "logging.FileHandler", + "formatter": "file", + "filename": "debug.log", + }, + }, + "loggers": { + "": { + "level": "DEBUG", + "handlers": ["console", "file"], + "propagate": True, + }, + "django.request": {"level": "DEBUG", "handlers": ["console", "file"]}, + }, +} + +sentry_sdk.init( + dsn="https://be8f804283b7a2e423209e00692792f0@o4506344044298240.ingest.sentry.io/4506344234156032", + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + traces_sample_rate=1.0, + # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0, +) + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +] + +SOCIALACCOUNT_PROVIDERS = { + 'github': { + 'SCOPE': [ + 'user', + 'repo', + 'read:org', + ], + }, + 'google': { + 'SCOPE': [ + 'profile', + 'email', + ], + 'AUTH_PARAMS': { + 'access_type': 'online', + }, + 'OAUTH_PKCE_ENABLED': True, + } +} \ No newline at end of file diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 376fa6a..e0214ff 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -8,19 +8,20 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), + path('accounts/', include('allauth.urls')), ] schema_view = get_schema_view( - openapi.Info( - title="Draft API", - default_version='v1', - description="Документация для приложения draft docuemnts проекта Шаблонизатор", - # terms_of_service="URL страницы с пользовательским соглашением", - contact=openapi.Contact(email="nikox122@mail.ru"), - license=openapi.License(name="BSD License"), - ), - public=True, - permission_classes=(permissions.AllowAny,), + openapi.Info( + title="Draft API", + default_version='v1', + description="Документация для приложения draft docuemnts проекта Шаблонизатор", + # terms_of_service="URL страницы с пользовательским соглашением", + contact=openapi.Contact(email="nikox122@mail.ru"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), ) urlpatterns += [ diff --git a/backend/commands/app.sh b/backend/commands/app.sh index 2578818..b94a237 100644 --- a/backend/commands/app.sh +++ b/backend/commands/app.sh @@ -1,4 +1,5 @@ #!/bin/bash + python manage.py makemigrations python manage.py migrate diff --git a/backend/core/constants.py b/backend/core/constants.py index 7158bb4..445c493 100644 --- a/backend/core/constants.py +++ b/backend/core/constants.py @@ -39,6 +39,21 @@ class Messages: TEMPLATE_LOAD_FINISHED: Final = ( "Загрузка завершена. Загружено {} шаблонов." ) + TEMPLATE_JSON_CORRUPTED: Final = "Ошибка в структуре json файла '{}'" FILE_NOT_FOUND: Final = "Файл '{}' не найден." UNKNOWN_GROUP_ID: Final = "Ошибка: неизвестный идентификатор группы '{}'" UNKNOWN_TYPE: Final = "Ошибка: неизвестный тип поля '{}'" + + TEMPLATE_EXCESS_TAGS: Final = ( + "Шаблон содержит тэги, для которых отсутствуют поля в базе" + ) + TEMPLATE_EXCESS_FIELDS: Final = ( + "В шаблоне отсутствуют тэги, для которых имеются поля в базе" + ) + TEMPLATE_CONSISTENT: Final = "Шаблон и поля согласованы" + TEMPLATE_FIELD_TAGS_ARE_NOT_UNIQUE: Final = ( + "Поля шаблона содержат неуникальные теги {}" + ) + TEMPLATE_GROUP_IDS_ARE_NOT_UNIQUE: Final = ( + "Группы полей шаблона содержат неуникальные идентификаторы id {}" + ) diff --git a/backend/core/management/commands/init_templates.py b/backend/core/management/commands/init_templates.py index e6565cc..909b718 100644 --- a/backend/core/management/commands/init_templates.py +++ b/backend/core/management/commands/init_templates.py @@ -7,6 +7,7 @@ from backend.settings import INITIAL_DATA_DIR +from api.v1.serializers import TemplateWriteSerializer from core.constants import Messages from documents.models import ( Template, @@ -18,6 +19,12 @@ TEMPLATE_LIST_SOURCE_FILE: str = "template_list.json" +def print_red(*args): + print("\033[31m", end="") + print(*args, end="") + print("\033[0m") + + def create_field_groups( group_list: List[Dict], template: Template ) -> Dict[int, TemplateFieldGroup]: @@ -58,38 +65,57 @@ def create_template_fields( def load_template(docx_file_name, json_file_name): + print("\n") print(Messages.TEMPLATE_LOADING.format(docx_file_name)) if not os.path.isfile(docx_file_name): - print(Messages.FILE_NOT_FOUND.format(docx_file_name)) + print_red(Messages.FILE_NOT_FOUND.format(docx_file_name)) return 0 if not os.path.isfile(json_file_name): - print(Messages.FILE_NOT_FOUND.format(json_file_name)) + print_red(Messages.FILE_NOT_FOUND.format(json_file_name)) return 0 with open(json_file_name, encoding="utf-8") as jsonfile: - context = json.load(jsonfile) - fields = context.pop("fields") - groups_list = context.pop("groups") - name = context.get("name") + try: + context = json.load(jsonfile) + except Exception as e: + print_red(Messages.TEMPLATE_JSON_CORRUPTED.format(json_file_name)) + print(e) + return 0 + new_docx_name = context.pop("template") + name = context.get("name", None) qs = Template.objects.filter(name=name) if qs.exists(): print(Messages.TEMPLATE_ALREADY_EXISTS.format(name), end="") choice = input() if choice == "1": - qs.delete() + try: + qs.delete() + except Exception as e: + print("Error at template delete operation!") + print(e) elif choice != "2": return 0 try: - template = Template(template="", **context) - template.save() + serializer = TemplateWriteSerializer(data=context) + if not serializer.is_valid(): + print_red( + Messages.TEMPLATE_JSON_CORRUPTED.format(json_file_name) + ) + print_red(serializer.errors) + return 0 + template = serializer.save() with open(docx_file_name, "rb") as f: template.template.save(new_docx_name, File(f)) + + # проверка консистентности загруженных полей и шаблона docx + errors = template.get_consistency_errors() + if errors: + print_red("Ошибки в шаблоне\n", errors) except Exception as e: - print("Error for data {}".format(context)) - print(e) + msg = "Error for data {}".format(json.dumps(context)) + print_red(msg) + print_red(e) return 0 - field_groups = create_field_groups(groups_list, template) - create_template_fields(fields, template, field_groups) print(Messages.TEMPLATE_LOADED.format(docx_file_name)) return 1 diff --git a/backend/core/template_render.py b/backend/core/template_render.py index 24cc00b..1d63fc9 100644 --- a/backend/core/template_render.py +++ b/backend/core/template_render.py @@ -107,6 +107,13 @@ def ablt(self, words: str) -> str: return value return self.inflect_words(words, "ablt") + def loct(self, words: str) -> str: + """Преобразует слова предложный падеж""" + skip, value = self._skip_filter(words) + if skip: + return value + return self.inflect_words(words, "loct") + def noun_plural(self, word: str, n: int) -> str: """Склонение заданного слова (существительное) в зависимости от числа n.""" skip, value = self._skip_filter(word) @@ -181,6 +188,7 @@ def get_filters(self): "genitive": self.genitive, "dative": self.dative, "ablt": self.ablt, + "loct": self.loct, "noun_plural": self.noun_plural, "adj_plural": self.adj_plural, "currency_to_words": self.currency_to_words, @@ -295,7 +303,7 @@ def _combine_styled_tag_runs(self, tag_style, runs): start_run = None def prepare_template(self): - """Подгтотовка шаблона к использованию (объединение прогонов)""" + """Подготовка шаблона к использованию (объединение прогонов)""" self._template.init_docx() docx = self._template.docx tag_style = docx.styles[self.TAG_STYLE_NAME] diff --git "a/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\270\314\206\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" "b/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\270\314\206\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" new file mode 100644 index 0000000..4a2cce5 Binary files /dev/null and "b/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\270\314\206\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" differ diff --git "a/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\271\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" "b/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\271\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" new file mode 100644 index 0000000..4a2cce5 Binary files /dev/null and "b/backend/data/preview/\320\224\320\276\320\263\320\276\320\262\320\276\321\200-\320\275\320\260\320\271\320\274\320\260-\320\266\320\270\320\273\320\276\320\263\320\276-\320\277\320\276\320\274\320\265\321\211\320\265\320\275\320\270\321\217-0001 1.jpg" differ diff --git "a/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\270\314\206-\321\201\320\260\320\264 2.jpg" "b/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\270\314\206-\321\201\320\260\320\264 2.jpg" new file mode 100644 index 0000000..e6b3d7c Binary files /dev/null and "b/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\270\314\206-\321\201\320\260\320\264 2.jpg" differ diff --git "a/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\271-\321\201\320\260\320\264 2.jpg" "b/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\271-\321\201\320\260\320\264 2.jpg" new file mode 100644 index 0000000..e6b3d7c Binary files /dev/null and "b/backend/data/preview/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\262-\320\224\320\265\321\202\321\201\320\272\320\270\320\271-\321\201\320\260\320\264 2.jpg" differ diff --git "a/backend/data/preview/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 1.jpg" "b/backend/data/preview/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 1.jpg" new file mode 100644 index 0000000..3526627 Binary files /dev/null and "b/backend/data/preview/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265 \320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 \320\270\320\267 \320\224\320\236\320\243 \321\200\320\276\320\264\321\201\321\202\320\262\320\265\320\275\320\275\320\270\320\272\320\260\320\274\320\270 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265 \320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 \320\270\320\267 \320\224\320\236\320\243 \321\200\320\276\320\264\321\201\321\202\320\262\320\265\320\275\320\275\320\270\320\272\320\260\320\274\320\270 1.jpg" new file mode 100644 index 0000000..acefd99 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265 \320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 \320\270\320\267 \320\224\320\236\320\243 \321\200\320\276\320\264\321\201\321\202\320\262\320\265\320\275\320\275\320\270\320\272\320\260\320\274\320\270 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265 \320\263\320\270\320\261\320\272\320\276\320\263\320\276 \320\263\321\200\320\260\321\204\320\270\320\272\320\260 \320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217 \320\234\320\221\320\224\320\236\320\243 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265 \320\263\320\270\320\261\320\272\320\276\320\263\320\276 \320\263\321\200\320\260\321\204\320\270\320\272\320\260 \320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217 \320\234\320\221\320\224\320\236\320\243 1.jpg" new file mode 100644 index 0000000..45ab331 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\275\320\260 \321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265 \320\263\320\270\320\261\320\272\320\276\320\263\320\276 \320\263\321\200\320\260\321\204\320\270\320\272\320\260 \320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217 \320\234\320\221\320\224\320\236\320\243 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\262\321\213\320\277\320\273\320\260\321\202\320\265 \320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270 2023 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\262\321\213\320\277\320\273\320\260\321\202\320\265 \320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270 2023 1.jpg" new file mode 100644 index 0000000..49c248c Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\262\321\213\320\277\320\273\320\260\321\202\320\265 \320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270 2023 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 \320\270\320\267 \320\264\321\200\321\203\320\263\320\276\320\263\320\276 \320\224\320\236\320\236 2023 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 \320\270\320\267 \320\264\321\200\321\203\320\263\320\276\320\263\320\276 \320\224\320\236\320\236 2023 1.jpg" new file mode 100644 index 0000000..f978b96 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 \320\270\320\267 \320\264\321\200\321\203\320\263\320\276\320\263\320\276 \320\224\320\236\320\236 2023 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 2023 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 2023 1.jpg" new file mode 100644 index 0000000..ea5c289 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276 \320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 \321\200\320\265\320\261\320\265\320\275\320\272\320\260 2023 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276\320\261 \320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276\320\261 \320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 1.jpg" new file mode 100644 index 0000000..8f1e92a Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265 \320\276\320\261 \320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\224\320\241.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\224\320\241.jpg" new file mode 100644 index 0000000..9bc3c3f Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\227\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\224\320\241.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\236\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265 \320\270\320\267 \320\224\320\236\320\243 \320\262 \320\224\320\236\320\243_\320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\236\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265 \320\270\320\267 \320\224\320\236\320\243 \320\262 \320\224\320\236\320\243_\320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 1.jpg" new file mode 100644 index 0000000..19e1133 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\236\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265 \320\270\320\267 \320\224\320\236\320\243 \320\262 \320\224\320\236\320\243_\320\262 \320\277\320\276\321\200\321\217\320\264\320\272\320\265 \320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\241\320\276\320\263\320\273\320\260\321\201\320\270\320\265 \320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\320\265 \320\264\320\260\320\275\320\275\321\213\320\265 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\241\320\276\320\263\320\273\320\260\321\201\320\270\320\265 \320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\320\265 \320\264\320\260\320\275\320\275\321\213\320\265 1.jpg" new file mode 100644 index 0000000..ce7f7b5 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\241\320\276\320\263\320\273\320\260\321\201\320\270\320\265 \320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\320\265 \320\264\320\260\320\275\320\275\321\213\320\265 1.jpg" differ diff --git "a/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\275\320\260-\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\320\241\320\236 1.jpg" "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\275\320\260-\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\320\241\320\236 1.jpg" new file mode 100644 index 0000000..401a621 Binary files /dev/null and "b/backend/data/preview/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265-\320\275\320\260-\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\320\241\320\236 1.jpg" differ diff --git "a/backend/data/preview/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272.jpg" "b/backend/data/preview/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272.jpg" new file mode 100644 index 0000000..b5dad30 Binary files /dev/null and "b/backend/data/preview/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\275\320\260 \320\262\320\276\320\267\320\262\321\200\320\260\321\202 \320\270\320\273\320\270 \320\276\320\261\320\274\320\265\320\275 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\275\320\260 \320\262\320\276\320\267\320\262\321\200\320\260\321\202 \320\270\320\273\320\270 \320\276\320\261\320\274\320\265\320\275 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260.jpg" new file mode 100644 index 0000000..033a789 Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\275\320\260 \320\262\320\276\320\267\320\262\321\200\320\260\321\202 \320\270\320\273\320\270 \320\276\320\261\320\274\320\265\320\275 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265 \320\264\320\265\320\275\320\265\320\263 \320\267\320\260 \320\261\320\270\320\273\320\265\321\202\321\213 \320\275\320\260 \320\272\320\276\320\275\321\206\320\265\321\200\321\202 2.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265 \320\264\320\265\320\275\320\265\320\263 \320\267\320\260 \320\261\320\270\320\273\320\265\321\202\321\213 \320\275\320\260 \320\272\320\276\320\275\321\206\320\265\321\200\321\202 2.jpg" new file mode 100644 index 0000000..27774a7 Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\230\321\201\321\205\320\276\320\264\320\275\320\270\320\272 \320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265 \320\264\320\265\320\275\320\265\320\263 \320\267\320\260 \320\261\320\270\320\273\320\265\321\202\321\213 \320\275\320\260 \320\272\320\276\320\275\321\206\320\265\321\200\321\202 2.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270 \321\203\321\211\320\265\321\200\320\261\320\260 \321\201\320\260\320\273\320\276\320\275\320\276\320\274 \320\272\321\200\320\260\321\201\320\276\321\202\321\213 2.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270 \321\203\321\211\320\265\321\200\320\261\320\260 \321\201\320\260\320\273\320\276\320\275\320\276\320\274 \320\272\321\200\320\260\321\201\320\276\321\202\321\213 2.jpg" new file mode 100644 index 0000000..06e6a80 Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270 \321\203\321\211\320\265\321\200\320\261\320\260 \321\201\320\260\320\273\320\276\320\275\320\276\320\274 \320\272\321\200\320\260\321\201\320\276\321\202\321\213 2.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270 \321\201\321\200\320\276\320\272\320\276\320\262 \320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217 \321\200\320\260\320\261\320\276\321\202) 2.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270 \321\201\321\200\320\276\320\272\320\276\320\262 \320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217 \321\200\320\260\320\261\320\276\321\202) 2.jpg" new file mode 100644 index 0000000..3b8c5ab Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270 \321\201\321\200\320\276\320\272\320\276\320\262 \320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217 \321\200\320\260\320\261\320\276\321\202) 2.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\277\321\200\320\276\320\264\320\260\320\266\320\265 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260 2.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\277\321\200\320\276\320\264\320\260\320\266\320\265 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260 2.jpg" new file mode 100644 index 0000000..3dcf2d6 Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\277\321\200\320\276\320\264\320\260\320\266\320\265 \321\202\320\276\320\262\320\260\321\200\320\260 \320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276 \320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260 2.jpg" differ diff --git "a/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205 \320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205 \321\200\320\260\320\261\320\276\321\202) 2.jpg" "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205 \320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205 \321\200\320\260\320\261\320\276\321\202) 2.jpg" new file mode 100644 index 0000000..c9f39ae Binary files /dev/null and "b/backend/data/preview/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\237\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217 \320\276 \320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205 \320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205 \321\203\321\201\320\273\321\203\320\263 (\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205 \321\200\320\260\320\261\320\276\321\202) 2.jpg" differ diff --git a/backend/data/template_field_types.json b/backend/data/template_field_types.json index 1340354..0166fa9 100644 --- a/backend/data/template_field_types.json +++ b/backend/data/template_field_types.json @@ -1,61 +1,73 @@ [ { "type": "fio", - "name": "Фамилия Имя Отчество" + "name": "Фамилия Имя Отчество", + "mask": "/^[\\w-]+ (?:[\\w-]+ )?[\\w-]+$/" }, { "type": "phone", - "name": "Номер телефона" + "name": "Номер телефона", + "mask": "/^\\+?\\d(?: ?\\(\\d+\\) ?)?[\\d -]+$/" }, { "type": "email", - "name": "Адрес email" + "name": "Адрес email", + "mask": "/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/" }, { "type": "date", - "name": "Дата" + "name": "Дата", + "mask": "/^(?:3[01]|[12][0-9]|0?[1-9]).(?:1[0-2]|0?[1-9]).(?:19|20)\\d\\d$/" }, { "type": "time", - "name": "Время" + "name": "Время", + "mask": "/^(?:[01]?[0-9]|2[0-3]):(?:[0-5][0-9])$/" }, { "type": "currency", - "name": "Сумма" + "name": "Сумма", + "mask": "/^[\\+-]?\\d+(?:\\.|,)?\\d?\\d?\\D*$/" }, { "type": "str", - "name": "Строка" + "name": "Строка", + "mask": "/^.*$/" }, { "type": "int", - "name": "Целочисленный" + "name": "Целочисленный", + "mask": "/^\\d*$/" }, { "type": "float", - "name": "Вещественный" + "name": "Вещественный", + "mask": "/^\\d+(?:\\.|,)?\\d*$/" }, { "type": "bool", - "name": "Логический" + "name": "Логический", + "mask": "/^true|false|да|нет|yes|no|истина|ложь$/i" }, { "type": "str20", - "name": "Строка(до 20 символов)" + "name": "Строка(до 20 символов)", + "mask": "/^.{20}$/" }, { "type": "str40", - "name": "Строка(до 40 символов)" + "name": "Строка(до 40 символов)", + "mask": "/^.{40}$/" }, { "type": "ddd_ddd", "name": "Код вида 123-456", - "mask": "^\\d{3}-\\d{3}$" + "mask": "/^\\d{3}-\\d{3}$/" }, { "type": "d10", "name": "Код из 10 цифр", - "mask": "^\\d{10}$" + "mask": "/^\\d{10}$/" }, { "type": "list_fio", diff --git a/backend/data/template_list.json b/backend/data/template_list.json index 1e8d0f4..65348b5 100644 --- a/backend/data/template_list.json +++ b/backend/data/template_list.json @@ -1,15 +1,91 @@ [ { - "template": "заявление_детсад_tpl.docx", - "fields": "заявление_детсад_tpl.json" + "template": "претензии/претензия_tpl.docx", + "fields": "претензии/претензия_tpl.json" }, { - "template": "заявление_на_отпуск_tpl.docx", - "fields": "заявление_на_отпуск_tpl.json" + "template": "претензии/претензия_к_медицинской_организации_о_некачественном_оказании_платной_медицинской_услуги_tpl.docx", + "fields": "претензии/претензия_к_медицинской_организации_о_некачественном_оказании_платной_медицинской_услуги_tpl.json" + }, + { + "template": "претензии/претензия_на_возврат_или_обмен_товара_надлежащего_качества_tpl.docx", + "fields": "претензии/претензия_на_возврат_или_обмен_товара_надлежащего_качества_tpl.json" + }, + { + "template": "претензии/претензия_о_возврате_денег_за_билеты_на_концерт_tpl.docx", + "fields": "претензии/претензия_о_возврате_денег_за_билеты_на_концерт_tpl.json" + }, + { + "template": "претензии/претензия_о_возмещении_туроператором_убытков_в_связи_с_задержкой_рейса_tpl.docx", + "fields": "претензии/претензия_о_возмещении_туроператором_убытков_в_связи_с_задержкой_рейса_tpl.json" + }, + { + "template": "претензии/претензия_о_возмещении_убытков_причиненных_задержкой_рейса_tpl.docx", + "fields": "претензии/претензия_о_возмещении_убытков_причиненных_задержкой_рейса_tpl.json" + }, + { + "template": "претензии/претензия_о_возмещении_ущерба_салоном_красоты_tpl.docx", + "fields": "претензии/претензия_о_возмещении_ущерба_салоном_красоты_tpl.json" + }, + { + "template": "претензии/претензия_о_нарушении_сроков_оказания_услуг_выполнения_работ_tpl.docx", + "fields": "претензии/претензия_о_нарушении_сроков_оказания_услуг_выполнения_работ_tpl.json" + }, + { + "template": "претензии/претензия_о_недостатках_оказанных_услуг_выполненных_работ_tpl.docx", + "fields": "претензии/претензия_о_недостатках_оказанных_услуг_выполненных_работ_tpl.json" + }, + { + "template": "претензии/претензия_о_продаже_товара_ненадлежащего_качества_tpl.docx", + "fields": "претензии/претензия_о_продаже_товара_ненадлежащего_качества_tpl.json" + }, + { + "template": "детский_сад/заявление_на_возврат_излишне_уплаченных_денежных_средств_tpl.docx", + "fields": "детский_сад/заявление_на_возврат_излишне_уплаченных_денежных_средств_tpl.json" + }, + { + "template": "детский_сад/заявление_на_отсутствие_по_со_tpl.docx", + "fields": "детский_сад/заявление_на_отсутствие_по_со_tpl.json" + }, + { + "template": "детский_сад/заявление_на_разрешение_забирать_ребенка_из_доу_tpl.docx", + "fields": "детский_сад/заявление_на_разрешение_забирать_ребенка_из_доу_tpl.json" + }, + { + "template": "детский_сад/заявление_на_установление_гибкого_графика_посещения_tpl.docx", + "fields": "детский_сад/заявление_на_установление_гибкого_графика_посещения_tpl.json" + }, + { + "template": "детский_сад/заявление_об_отчислении_из_мбдоу_tpl.docx", + "fields": "детский_сад/заявление_об_отчислении_из_мбдоу_tpl.json" + }, + { + "template": "детский_сад/отчисление_из_доу_в_порядке_перевода_tpl.docx", + "fields": "детский_сад/отчисление_из_доу_в_порядке_перевода_tpl.json" + }, + { + "template": "детский_сад/заявление_о_выплате_компенсации_2023_tpl.docx", + "fields": "детский_сад/заявление_о_выплате_компенсации_2023_tpl.json" }, { - "template": "претензия_tpl.docx", - "fields": "претензия_tpl.json" + "template": "детский_сад/заявление_о_зачислении_ребенка_в_мбдоу_tpl.docx", + "fields": "детский_сад/заявление_о_зачислении_ребенка_в_мбдоу_tpl.json" + }, + { + "template": "детский_сад/заявление_о_зачислении_в_порядке_перевода_tpl.docx", + "fields": "детский_сад/заявление_о_зачислении_в_порядке_перевода_tpl.json" + }, + { + "template": "детский_сад/согласие_на_обработку_персональных_данных_tpl.docx", + "fields": "детский_сад/согласие_на_обработку_персональных_данных_tpl.json" + }, + { + "template": "детский_сад/заявление_детсад_tpl.docx", + "fields": "детский_сад/заявление_детсад_tpl.json" + }, + { + "template": "заявление_на_отпуск_tpl.docx", + "fields": "заявление_на_отпуск_tpl.json" }, { "template": "договор_найма_жилого_помещения_tpl.docx", diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" new file mode 100644 index 0000000..e20c8c5 Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" differ diff --git "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" similarity index 93% rename from "backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" rename to "backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" index 1405985..82af097 100644 --- "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.json" @@ -57,7 +57,7 @@ "tag": "Дата1", "name": "Дата начала отпуска", "hint": "дд.мм.гггг", - "group": null, + "group": 5, "type": "date", "length": 40 }, @@ -65,7 +65,7 @@ "tag": "Дата2", "name": "Дата окончания отпуска", "hint": "дд.мм.гггг", - "group": null, + "group": 5, "type": "date", "length": 40 }, @@ -73,7 +73,7 @@ "tag": "Дата3", "name": "Дата подачи заявления", "hint": "дд.мм.гггг", - "group": null, + "group": 5, "type": "date", "length": 40 } @@ -94,6 +94,10 @@ { "id": 4, "name": "Ребенок" + }, + { + "id": 5, + "name": "Даты" } ] } diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.docx" new file mode 100644 index 0000000..9a7ba2c Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.json" new file mode 100644 index 0000000..d776ac4 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\267\320\273\320\270\321\210\320\275\320\265_\321\203\320\277\320\273\320\260\321\207\320\265\320\275\320\275\321\213\321\205_\320\264\320\265\320\275\320\265\320\266\320\275\321\213\321\205_\321\201\321\200\320\265\320\264\321\201\321\202\320\262_tpl.json" @@ -0,0 +1,91 @@ +{ + "template": "заявление_на_возврат_излишне_уплаченных_денежных_средств_tpl.docx", + "name": "Заявление на возврат излишне уплаченных средств", + "deleted": "False", + "description": "Заявление на возврат излишне уплаченных средств", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "Сумма", + "name": "Сумма возврата в рублях", + "hint": "1217.54", + "group": 3, + "type": "currency", + "length": 40 + }, + { + "tag": "НомерРСчета", + "name": "Номер Расчетного счета", + "hint": "4231 1111 1111 1111 1111", + "group": 3, + "type": "str20", + "length": 20 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 10 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Данные возврата средств" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.docx" new file mode 100644 index 0000000..78e5a13 Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.json" new file mode 100644 index 0000000..e8f059e --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\321\201\321\203\321\202\321\201\321\202\320\262\320\270\320\265_\320\277\320\276_\321\201\320\276_tpl.json" @@ -0,0 +1,115 @@ +{ + "template": "заявление_на_отсутствие_по_со_tpl.docx", + "name": "Заявление на отсутствие по семейным обстоятельствам", + "deleted": "False", + "description": "Заявление в детский сад на отсутствие по семейным обстоятельствам", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресПроживанияРодителя", + "name": "Адрес проживания родителя", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 20 + }, + { + "tag": "ФИОРебенкаДатПадеж", + "name": "ФИО Ребенка в дательном падеже", + "hint": "Васильеву Павлу Сергеевичу", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ГруппаДС", + "name": "Название или номер группы детского сада", + "hint": "Ладошки", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "ДатаНачала", + "name": "Дата начала посещения по гибкому графику", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 30 + }, + { + "tag": "ДатаОкончания", + "name": "Дата окончания посещения по гибкому графику", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 30 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя в именительном падеже", + "hint": "Иванов Иван Иванович", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Информация о днях отсутствия" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.docx" new file mode 100644 index 0000000..58209db Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.json" new file mode 100644 index 0000000..0261416 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\200\320\260\320\267\321\200\320\265\321\210\320\265\320\275\320\270\320\265_\320\267\320\260\320\261\320\270\321\200\320\260\321\202\321\214_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\270\320\267_\320\264\320\276\321\203_tpl.json" @@ -0,0 +1,160 @@ +{ + "template": "заявление_на_разрешение_забирать_ребенка_из_доу_tpl.docx", + "name": "Заявление на разрешение забирать ребенка из ДОУ", + "deleted": "False", + "description": "Заявление на разрешение забирать ребенка из ДОУ", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "ГруппаДС", + "name": "Название или номер группы детского сада", + "hint": "Ладошки", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "ФИО1", + "name": "ФИО первого доверенного лица", + "hint": "Иванов Иван Иванович", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "Родство1", + "name": "Степень родства первого доверенного лица", + "hint": "Бабушка", + "group": 3, + "type": "str20", + "length": 20 + }, + { + "tag": "ДатаРожд1", + "name": "Дата рождения первого доверенного лица", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + }, + { + "tag": "ФИО2", + "name": "ФИО второго доверенного лица", + "hint": "Иванов Иван Иванович", + "group": 3, + "type": "str", + "length": 100, + "default": " " + }, + { + "tag": "Родство2", + "name": "Степень родства второго доверенного лица", + "hint": "Бабушка", + "group": 3, + "type": "str20", + "length": 20, + "default": " " + }, + { + "tag": "ДатаРожд2", + "name": "Дата рождения второго доверенного лица", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20, + "default": " " + }, + { + "tag": "ФИО3", + "name": "ФИО третьего доверенного лица", + "hint": "Иванов Иван Иванович", + "group": 3, + "type": "str", + "length": 100, + "default": " " + }, + { + "tag": "Родство3", + "name": "Степень родства третьего доверенного лица", + "hint": "Бабушка", + "group": 3, + "type": "str20", + "length": 20, + "default": " " + }, + { + "tag": "ДатаРожд3", + "name": "Дата рождения третьего доверенного лица", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20, + "default": " " + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 20 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Данные доверенных лиц" + }, + { + "id": 4, + "name": "Даты" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.docx" new file mode 100644 index 0000000..58fe668 Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.json" new file mode 100644 index 0000000..78fae63 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\321\203\321\201\321\202\320\260\320\275\320\276\320\262\320\273\320\265\320\275\320\270\320\265_\320\263\320\270\320\261\320\272\320\276\320\263\320\276_\320\263\321\200\320\260\321\204\320\270\320\272\320\260_\320\277\320\276\321\201\320\265\321\211\320\265\320\275\320\270\321\217_tpl.json" @@ -0,0 +1,123 @@ +{ + "template": "заявление_на_установление_гибкого_графика_посещения_tpl.docx", + "name": "Заявление на установление гибкого графика посещения", + "deleted": "False", + "description": "Заявление на установление гибкого графика посещения детского сада", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРебенкаДатПадеж", + "name": "ФИО Ребенка в дательном падеже", + "hint": "Васильеву Павлу Сергеевичу", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "ГруппаДС", + "name": "Название или номер группы детского сада", + "hint": "Ладошки", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "Причина", + "name": "В связи с чем переходит на гибкий график", + "hint": "посещением дополнительных занятий", + "group": 3, + "type": "str", + "length": 254 + }, + { + "tag": "ДатаНачала", + "name": "Дата начала посещения по гибкому графику", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + }, + { + "tag": "ДатаОкончания", + "name": "Дата окончания посещения по гибкому графику", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + }, + { + "tag": "ВремяНачала", + "name": "Время начала пребывания в детском саду", + "hint": "чч:мм", + "group": 3, + "type": "time", + "length": 5 + }, + { + "tag": "ВремяОкончания", + "name": "Время окончания пребывания в детском саду", + "hint": "чч:мм", + "group": 3, + "type": "time", + "length": 5 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 20 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Информация о гибком графике" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.docx" new file mode 100644 index 0000000..f2056a8 Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.json" new file mode 100644 index 0000000..4ac0104 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\262\321\213\320\277\320\273\320\260\321\202\320\265_\320\272\320\276\320\274\320\277\320\265\320\275\321\201\320\260\321\206\320\270\320\270_2023_tpl.json" @@ -0,0 +1,187 @@ +{ + "template": "заявление_о_выплате_компенсации_2023_tpl.docx", + "name": "Заявление о выплате компенсации 2023", + "deleted": "False", + "description": "Заявление о выплате компенсации 2023", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресРегистрацииРодителя", + "name": "Адрес регистрации родителя", + "hint": "г. Сызрань, ул. Тульская, 4, кв. 12", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "СерияНомерПаспортаРодителя", + "name": "Серия и номер паспорта родителя", + "hint": "0101 010101", + "group": 2, + "type": "d10", + "length": 30 + }, + { + "tag": "ДатаВыдачиПаспорта", + "name": "Дата выдачи паспорта", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "КемВыданПаспорт", + "name": "Наименование организации, выдавшей паспорт", + "hint": "ГУ МВД г. Сызрань", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 30 + }, + { + "tag": "EmailРодителя", + "name": "E-mail родителя", + "hint": "example@mail.ru", + "group": 2, + "type": "email", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "РебенокПоСчету", + "name": "Какой по счету ребенок в семье?", + "hint": "2", + "group": 2, + "type": "int", + "length": 3 + }, + { + "tag": "ПолныйАдресРегистрацииРебенка", + "name": "Полный адрес регистрации ребенка", + "hint": "г. Сызрань, ул. Тульская, 4, кв. 12", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ПолныйАдресПроживанияРебенка", + "name": "Полный адрес проживания ребенка", + "hint": "г. Сызрань, ул. Тульская, 4, кв. 12", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "НомерРасчетногоСчета", + "name": "Номер расчетного счета для выплаты компенсации", + "hint": "1111 1111 1111 1111 1111", + "group": 3, + "type": "str", + "length": 30 + }, + { + "tag": "НаименованиеКредитнойОрганизации", + "name": "Наименование кредитной организации", + "hint": "Сбербанк", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СНИЛСРебенка", + "name": "Номер СНИЛС ребенка", + "hint": "111-111-111 11", + "group": 2, + "type": "str", + "length": 30 + }, + { + "tag": "СНИЛСРодителя", + "name": "Номер СНИЛС родителя", + "hint": "111-111-111 11", + "group": 2, + "type": "str", + "length": 20 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя в именительном падеже", + "hint": "Иванова Инна Ивановна", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Информация о выплате компенсации" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" new file mode 100644 index 0000000..5943e3e Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" new file mode 100644 index 0000000..7f75416 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" @@ -0,0 +1,235 @@ +{ + "template": "заявление_о_зачислении_в_порядке_перевода_tpl.docx", + "name": "Заявление о зачислении в порядке перевода из другого ДОУ", + "deleted": "False", + "description": "Заявление о зачислении в порядке перевода", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада в который зачисляется ребенок", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя (законного представителя) в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресРегистрацииРодителя", + "name": "Адрес регистрации родителя (законного представителя)", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "СерияНомерПаспортаРодителя", + "name": "Серия и номер паспорта родителя (законного представителя)", + "hint": "0101 010101", + "group": 2, + "type": "d10", + "length": 30 + }, + { + "tag": "ДатаВыдачиПаспорта", + "name": "Дата выдачи паспорта", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "КемВыданПаспорт", + "name": "Наименование организации, выдавшей паспорт", + "hint": "ГУ МВД г. Сызрань", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 30 + }, + { + "tag": "EmailРодителя", + "name": "E-mail родителя (законного представителя)", + "hint": "example@mail.ru", + "group": 2, + "type": "email", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "НаименованиеОрганизацииВыбывыния", + "name": "Наименование детского сада, из которого отчисляется ребенок", + "hint": "МБДОУ № 12", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "MinВозраст", + "name": "Минимальный возраст детей в группе", + "hint": "4", + "group": 1, + "type": "int", + "length": 1 + }, + { + "tag": "MaxВозраст", + "name": "Максимальный возраст детей в группе", + "hint": "5", + "group": 1, + "type": "int", + "length": 1 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "МестоРожденияРебенка", + "name": "Место рождения ребенка", + "hint": "с. Усть-Авам Дудинского района", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресПроживанияРебенка", + "name": "Фактический адрес проживания ребенка", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаЗачисленияРебенка", + "name": "Дата зачисления ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + }, + { + "tag": "ФИОМатери", + "name": "ФИО матери в именительном падеже", + "hint": "Иванова Анна Ивановна", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "АдресМатери", + "name": "Адрес матери", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ТелефонМатери", + "name": "Телефон матери", + "hint": "+79111111111", + "group": 3, + "type": "phone", + "length": 30 + }, + { + "tag": "ФИООтца", + "name": "ФИО отца в именительном падеже", + "hint": "Иванова Андрей Иванович", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "АдресОтца", + "name": "Адрес отца", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ТелефонОтца", + "name": "Телефон отца", + "hint": "+79111111111", + "group": 3, + "type": "phone", + "length": 30 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя (законное представителя) в именительном падеже", + "hint": "Иванов Иван Иванович", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ЯзыкВоспитанияВСаду", + "name": "Языка воспитания в саду", + "hint": "русский", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детских садов" + }, + { + "id": 2, + "name": "Данные родителя (законного представителя) и ребенка" + }, + { + "id": 3, + "name": "Данные родителей ребенка" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.docx" new file mode 100644 index 0000000..592db0d Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.json" new file mode 100644 index 0000000..d0601f3 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276_\320\267\320\260\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\321\200\320\265\320\261\320\265\320\275\320\272\320\260_\320\262_\320\274\320\261\320\264\320\276\321\203_tpl.json" @@ -0,0 +1,226 @@ +{ + "template": "заявление_о_зачислении_ребенка_в_мбдоу_tpl.docx", + "name": "Заявление о зачислении ребенка в МБДОУ", + "deleted": "False", + "description": "Заявление о зачислении ребенка в МБДОУ", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада в который зачисляется ребенок", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя (законного представителя) в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресРегистрацииРодителя", + "name": "Адрес регистрации родителя (законного представителя)", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "СерияНомерПаспортаРодителя", + "name": "Серия и номер паспорта родителя (законного представителя)", + "hint": "0101 010101", + "group": 2, + "type": "d10", + "length": 30 + }, + { + "tag": "ДатаВыдачиПаспорта", + "name": "Дата выдачи паспорта", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "КемВыданПаспорт", + "name": "Наименование организации, выдавшей паспорт", + "hint": "ГУ МВД г. Сызрань", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 30 + }, + { + "tag": "EmailРодителя", + "name": "E-mail родителя (законного представителя)", + "hint": "example@mail.ru", + "group": 2, + "type": "email", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "MinВозраст", + "name": "Минимальный возраст детей в группе", + "hint": "4", + "group": 1, + "type": "int", + "length": 1 + }, + { + "tag": "MaxВозраст", + "name": "Максимальный возраст детей в группе", + "hint": "5", + "group": 1, + "type": "int", + "length": 1 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "МестоРожденияРебенка", + "name": "Место рождения ребенка", + "hint": "с. Усть-Авам Дудинского района", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресПроживанияРебенка", + "name": "Фактический адрес проживания ребенка", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаЗачисленияРебенка", + "name": "Дата зачисления ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + }, + { + "tag": "ФИОМатери", + "name": "ФИО матери в именительном падеже", + "hint": "Иванова Анна Ивановна", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "АдресМатери", + "name": "Адрес матери", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ТелефонМатери", + "name": "Телефон матери", + "hint": "+79111111111", + "group": 3, + "type": "phone", + "length": 30 + }, + { + "tag": "ФИООтца", + "name": "ФИО отца в именительном падеже", + "hint": "Иванова Андрей Иванович", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "АдресОтца", + "name": "Адрес отца", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ТелефонОтца", + "name": "Телефон отца", + "hint": "+79111111111", + "group": 3, + "type": "phone", + "length": 30 + }, + { + "tag": "ЯзыкВоспитанияВСаду", + "name": "На каком языке разговаривают в детском саду", + "hint": "русском", + "group": 1, + "type": "str20", + "length": 30 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя (законное представителя) в именительном падеже", + "hint": "Иванов Иван Иванович", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 30 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя (законного представителя) и ребенка" + }, + { + "id": 3, + "name": "Данные родителей ребенка" + }, + { + "id": 4, + "name": "Даты" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.docx" new file mode 100644 index 0000000..1de0827 Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.json" new file mode 100644 index 0000000..c412343 --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\276\320\261_\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\270_\320\270\320\267_\320\274\320\261\320\264\320\276\321\203_tpl.json" @@ -0,0 +1,119 @@ +{ + "template": "заявление_об_отчислении_из_мбдоу_tpl.docx", + "name": "Заявление об отчислении из МБДОУ", + "deleted": "False", + "description": "Заявление об отчислении из МБДОУ", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресПроживанияРодителя", + "name": "Адрес проживания родителя", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 20 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "ГруппаДС", + "name": "Название или номер группы детского сада", + "hint": "Ладошки", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "ПричинаОтчисления", + "name": "Указать причину отчисления", + "hint": "слишком далеко от дома", + "group": 3, + "type": "str", + "length": 254 + }, + { + "tag": "ДатаОтчисленияРебенка", + "name": "Дата отчисления ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя в именительном падеже", + "hint": "Иванов Иван Иванович", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя (законного представителя) и ребенка" + }, + { + "id": 3, + "name": "Данные заявления" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" new file mode 100644 index 0000000..855a38c Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" new file mode 100644 index 0000000..f9bef6a --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\320\276\321\202\321\207\320\270\321\201\320\273\320\265\320\275\320\270\320\265_\320\270\320\267_\320\264\320\276\321\203_\320\262_\320\277\320\276\321\200\321\217\320\264\320\272\320\265_\320\277\320\265\321\200\320\265\320\262\320\276\320\264\320\260_tpl.json" @@ -0,0 +1,127 @@ +{ + "template": "отчисление_из_доу_в_порядке_перевода_tpl.docx", + "name": "Отчисление из ДОУ в порядке перевода в другой ДОУ", + "deleted": "False", + "description": "Отчисление из ДОУ в порядке перевода в другой ДОУ", + "fields": [ + { + "tag": "НаименованиеОрганизации", + "name": "Наименование Детского сада, из которого отчисляется ребенок", + "hint": "МБДОУ № 364", + "group": 1, + "type": "str", + "length": 100 + + }, + { + "tag": "ФИОЗаведующегоДатПадеж", + "name": "ФИО Заведующего в дательном падеже", + "hint": "Ивановой Тамаре Ивановне", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителяРодПадеж", + "name": "ФИО Родителя в родительном падеже", + "hint": "Васильевой Ирины Ивановны", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресПроживанияРодителя", + "name": "Адрес проживания родителя", + "hint": "г. Пермь, ул. Шахтеров, д. 34, кв. 11", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "КонтактныйТелефон", + "name": "Контактный телефон", + "hint": "+71111111111", + "group": 2, + "type": "phone", + "length": 30 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "НаименованиеОрганизацииЗачисления", + "name": "Наименование детского сада, в который переводится ребенок", + "hint": "МБДОУ № 12", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 2, + "type": "date", + "length": 30 + }, + { + "tag": "ДатаПоследнегоПосещения", + "name": "Дата последнего посещения ребенка", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 30 + }, + { + "tag": "ГруппаДС", + "name": "Название или номер группы детского сада", + "hint": "Ладошки", + "group": 1, + "type": "str40", + "length": 50 + }, + { + "tag": "ПричинаОтчисления", + "name": "Указать причину отчисления", + "hint": "далеко ездить до детского сада", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя в именительном падеже", + "hint": "Иванов Иван Иванович", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 30 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детских садов" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Данные заявления" + } + ] +} diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.docx" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.docx" new file mode 100644 index 0000000..cd70c1a Binary files /dev/null and "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.docx" differ diff --git "a/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.json" "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.json" new file mode 100644 index 0000000..6f6d0eb --- /dev/null +++ "b/backend/data/\320\264\320\265\321\202\321\201\320\272\320\270\320\271_\321\201\320\260\320\264/\321\201\320\276\320\263\320\273\320\260\321\201\320\270\320\265_\320\275\320\260_\320\276\320\261\321\200\320\260\320\261\320\276\321\202\320\272\321\203_\320\277\320\265\321\200\321\201\320\276\320\275\320\260\320\273\321\214\320\275\321\213\321\205_\320\264\320\260\320\275\320\275\321\213\321\205_tpl.json" @@ -0,0 +1,148 @@ +{ + "template": "согласие_на_обработку_персональных_данных_tpl.docx", + "name": "Согласие на обработку персональных данных", + "deleted": "False", + "description": "Согласие на обработку персональных данных", + "fields": [ + { + "tag": "НаименованиеОрганизацииРодПадеж", + "name": "Полное наименование дошкольного учреждения в родительном падеже", + "hint": "Муниципального дошкольного образовательного учреждения № 21", + "group": 1, + "type": "str", + "length": 150 + + }, + { + "tag": "ФИОРодителя", + "name": "ФИО родителя в именительном падеже", + "hint": "Иванова Инна Ивановна", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОРебенкаРодПадеж", + "name": "ФИО Ребенка в родительном падеже", + "hint": "Васильева Павла Сергеевича", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "АдресРегистрацииРодителя", + "name": "Адрес регистрации родителя", + "hint": "г. Сызрань, ул. Тульская, 4, кв. 12", + "group": 2, + "type": "str", + "length": 200 + }, + { + "tag": "СерияНомерПаспорта", + "name": "Серия и номер паспорта родителя", + "hint": "0101 010101", + "group": 2, + "type": "d10", + "length": 10 + }, + { + "tag": "КемВыданПаспорт", + "name": "Наименование организации, выдавшей паспорт", + "hint": "ГУ МВД г. Сызрань", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаВыдачиПаспорта", + "name": "Дата выдачи паспорта", + "hint": "dd.mm.yyyy", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "НаименованиеОрганизацииДатПадеж", + "name": "Наименование детского сада в дательном падеже", + "hint": "Муниципальному дошкольному образовательному учреждению № 21", + "group": 1, + "type": "str", + "length": 150 + + }, + { + "tag": "ФИОЗаведующегоРодПадеж", + "name": "ФИО Заведующего в родительном падеже", + "hint": "Ивановой Тамары Ивановны", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаРожденияРебенка", + "name": "Дата рождения ребенка", + "hint": "dd.mm.yyyy", + "group": 2, + "type": "date", + "length": 20 + }, + { + "tag": "СНИЛСРебенка", + "name": "Номер СНИЛС ребенка", + "hint": "111-111-111 11", + "group": 2, + "type": "str", + "length": 20 + }, + { + "tag": "СНИЛСРодителя", + "name": "Номер СНИЛС родителя", + "hint": "111-111-111 11", + "group": 2, + "type": "str", + "length": 20 + }, + { + "tag": "EmailРодителя", + "name": "E-mail родителя", + "hint": "example@mail.ru", + "group": 2, + "type": "email", + "length": 100 + }, + { + "tag": "URLСада", + "name": "Адрес сайта детского сада", + "hint": "https://мбдоу21.ru", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подачи заявления", + "hint": "dd.mm.yyyy", + "group": 4, + "type": "date", + "length": 20 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные детского сада" + }, + { + "id": 2, + "name": "Данные родителя и ребенка" + }, + { + "id": 3, + "name": "Информация о сборе персональных данных" + }, + { + "id": 4, + "name": "Даты" + } + ] +} diff --git "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" "b/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" deleted file mode 100644 index f340820..0000000 Binary files "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\264\320\265\321\202\321\201\320\260\320\264_tpl.docx" and /dev/null differ diff --git "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.docx" "b/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.docx" index cf69602..30ab5aa 100644 Binary files "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.docx" and "b/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.docx" differ diff --git "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.json" "b/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.json" index 48fa371..024a099 100644 --- "a/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.json" +++ "b/backend/data/\320\267\320\260\321\217\320\262\320\273\320\265\320\275\320\270\320\265_\320\275\320\260_\320\276\321\202\320\277\321\203\321\201\320\272_tpl.json" @@ -67,6 +67,14 @@ "group": 3, "type": "currency", "length": 40 + }, + { + "tag": "дата", + "name": "Дата заявления", + "hint": "ДД.ММ.ГГГГ (10.02.2023)", + "group": 3, + "type": "date", + "length": 40 } ], "groups": [ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" new file mode 100644 index 0000000..a2f2edf Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.json" similarity index 100% rename from "backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.json" rename to "backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.json" diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.docx" new file mode 100644 index 0000000..5087a1f Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.json" new file mode 100644 index 0000000..1805551 --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\272_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\320\276\321\200\320\263\320\260\320\275\320\270\320\267\320\260\321\206\320\270\320\270_\320\276_\320\275\320\265\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\265\320\275\320\275\320\276\320\274_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\320\270_\320\277\320\273\320\260\321\202\320\275\320\276\320\271_\320\274\320\265\320\264\320\270\321\206\320\270\320\275\321\201\320\272\320\276\320\271_\321\203\321\201\320\273\321\203\320\263\320\270_tpl.json" @@ -0,0 +1,162 @@ +{ + "template": "претензия_к_медицинской_организации_о_некачественном_оказании_платной_медицинской_услуги_tpl.docx", + "name": "Претензия к медицинской организации о некачественном оказании платной медицинской услуги", + "deleted": "False", + "description": "Данный шаблон поможет обратиться за возмещенииемм в медицинское учреждение о некачественном оказании платной медицинской услуги.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаЗаключенияДоговора", + "name": "Дата заключения договора", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПерваяСторонаФИОтворПадеж", + "name": "Первая сторона (ФИО в творительном падеже)", + "hint": "Ивановым Алексеем Дмитриевичем", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "ВтораяСторонаФИОилиОргТворПадеж", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "ФИО или ООО \"Ромашка\"", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НомерДоговора", + "name": "Номер договора", + "hint": "№ 12345", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СуммаОказанныхУслуг", + "name": "Стоимость услуг (работ)", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ПодтверждающиеДокументыТворительныйПадеж", + "name": "Подтверждающие документы", + "hint": "квитанцией, чеком", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата подтверждающего документа", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "СтатусИсполненияОбязательств", + "name": "Статус исполнения обязательств", + "hint": "Не исполнены в установленный договором срок, исполнены частично", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "Безвозмездно устранить недостатки оказанной услуги, возместить понесенные расходы по устранению недостатков оказанной услуги", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы", + "hint": "Копия кассового чека", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" new file mode 100644 index 0000000..99a85ce Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" new file mode 100644 index 0000000..7bfe762 --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\275\320\260_\320\262\320\276\320\267\320\262\321\200\320\260\321\202_\320\270\320\273\320\270_\320\276\320\261\320\274\320\265\320\275_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" @@ -0,0 +1,146 @@ +{ + "template": "претензия_на_возврат_или_обмен_товара_надлежащего_качества_tpl.docx", + "name": "Претензия на возврат или обмен товара надлежащего качества", + "deleted": "False", + "description": "Данный шаблон поможет запросить возврат или обмен товара надлежащего качества.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаПриобретенияТовара", + "name": "Дата приобретения товара", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "НаименованиеТовара", + "name": "Наименование товара", + "hint": "ботинки, платье и т.д.", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ПодтверждающиеДокументы", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "Кассовым чеком, товарным чеком", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ПричинаНевозможностиИспользования", + "name": "Причина невозможности использования", + "hint": "не подходит по фасону, габаритам, размеру", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаОбращенияПоОбмену", + "name": "Дата обращения по поводу обмена", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 20 + }, + { + "tag": "НеобходимыйКритерийТовара", + "name": "Необходимый критерий товара (в родительном падеже)", + "hint": "размера, габарита, фасона, формы, расцветки, комплектации", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "обменять на товар, соответствующий критериям, или произвести возврат", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы", + "hint": "Копия кассового чека", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Информация о товаре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.docx" new file mode 100644 index 0000000..d088b02 Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.json" new file mode 100644 index 0000000..508194c --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\262\321\200\320\260\321\202\320\265_\320\264\320\265\320\275\320\265\320\263_\320\267\320\260_\320\261\320\270\320\273\320\265\321\202\321\213_\320\275\320\260_\320\272\320\276\320\275\321\206\320\265\321\200\321\202_tpl.json" @@ -0,0 +1,178 @@ +{ + "template": "претензия_о_возврате_денег_за_билеты_на_концерт_tpl.docx", + "name": "Претензия о возврате денег за билеты на концерт", + "deleted": "False", + "description": "Этот шаблон поможет вам запросить возврат средств за приобретенные билеты на концерт, отмененный из-за определенных обстоятельств.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО Ромашка", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаПокупкиБилета", + "name": "Дата покупки билета", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "СайтПокупкиБилета", + "name": "Сайт покупки билета", + "hint": "www.afisha.yandex.ru", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СтоимостьБилета", + "name": "Стоимость билета", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 40 + }, + { + "tag": "КомиссияСервиса", + "name": "Комиссия сервиса", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 40 + }, + { + "tag": "ПодтверждающиеДокументыТворПадеж", + "name": "Подтверждающие документы в творительном падеже", + "hint": "Чеком об оплате", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НаименованиеКонцерта", + "name": "Наименование концерта", + "hint": "FEDUK", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ОрганизаторКонцерта", + "name": "Организатор концерта", + "hint": "MusicToThe", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НомерЗаказа", + "name": "Номер заказа", + "hint": "5657206541", + "group": 3, + "type": "int", + "length": 100 + }, + { + "tag": "НомерБилета", + "name": "Номер билета", + "hint": "БХ352647", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаПереносаКонцерта", + "name": "Дата переноса концерта", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Соодержание предъявляемых требований", + "hint": "Возместить стоимость билета, включая комиссию сервиса", + "group": 4, + "type": "int", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы", + "hint": "Копия кассового чека", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.docx" new file mode 100644 index 0000000..88342f5 Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.json" new file mode 100644 index 0000000..67d41f7 --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\202\321\203\321\200\320\276\320\277\320\265\321\200\320\260\321\202\320\276\321\200\320\276\320\274_\321\203\320\261\321\213\321\202\320\272\320\276\320\262_\320\262_\321\201\320\262\321\217\320\267\320\270_\321\201_\320\267\320\260\320\264\320\265\321\200\320\266\320\272\320\276\320\271_\321\200\320\265\320\271\321\201\320\260_tpl.json" @@ -0,0 +1,218 @@ +{ + "template": "претензия_о_возмещении_туроператором_убытков_в_связи_с_задержкой_рейса_tpl.docx", + "name": "Претензия о возмещении туроператором убытков в связи с задержкой рейса", + "deleted": "False", + "description": "Этот шаблон поможет Вам официально выразить недовольство качеством полученных услуг и разрешить конфликты.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаЗаключенияДоговора", + "name": "Дата заключения договора", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПерваяСторонаФИОтворПадеж", + "name": "Первая сторона (ФИО в творительном падеже)", + "hint": "Ивановым Алексеем Дмитриевичем", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "ВтораяСторонаФИОилиОргТворПадеж", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "ООО \"Ромашка\"", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НомерДоговора", + "name": "Номер договора", + "hint": "12345", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "КоличествоДнейТура", + "name": "Количество дней тура", + "hint": "12", + "group": 3, + "type": "int", + "length": 100 + }, + { + "tag": "ДатаНачалаТура", + "name": "Дата начала тура", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ДатаОкончанияТура", + "name": "Дата окончания тура", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "СтоимостьТура", + "name": "Стоимость тура", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ВремяЗадержкиРейса", + "name": "Подтверждающие документы", + "hint": "1 ч 30 мин", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ФактическоеВремяВылетаРейса", + "name": "Фактическое время вылета рейса", + "hint": "2:35", + "group": 3, + "type": "time", + "length": 40 + }, + { + "tag": "ФактическоеДатаВылетаРейса", + "name": "Фактическая дата вылета рейса", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПодтверждающиеДокументыТворительныйПадеж", + "name": "Подтверждающие документы", + "hint": "Справка об отмене рейса", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОдательныйПадеж", + "name": "ФИО (в дательном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "СуммаМатериальныхУбытков", + "name": "Сумма материальных убытков", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ДокументыПодтверждающиеУбыткиВтворительномПадеже", + "name": "Документы подтверждающие убытки (в творительном падеже)", + "hint": "Справкой о переносе рейса", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "Возместить стоимость авиабилета, материальные убытки", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "Документы", + "name": "Документы, прикладываемые к претензии", + "hint": "Копия договора, копия маршрутной квитанции и т.д.", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.docx" new file mode 100644 index 0000000..bfae0a8 Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.json" new file mode 100644 index 0000000..bd3c921 --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\262\320\276\320\267\320\274\320\265\321\211\320\265\320\275\320\270\320\270_\321\203\321\211\320\265\321\200\320\261\320\260_\321\201\320\260\320\273\320\276\320\275\320\276\320\274_\320\272\321\200\320\260\321\201\320\276\321\202\321\213_tpl.json" @@ -0,0 +1,198 @@ +{ + "template": "претензия_о_возмещении_ущерба_салоном_красоты_tpl.docx", + "name": "Претензия о возмещении ущерба салоном красоты", + "deleted": "False", + "description": "Этот шаблон поможет вам запросить компенсацию за ущерб, причиненный в салоне красоты, и защитить свои права как клиента", + "fields": [ + { + "tag": "НаименованиеОрганизацииИлиФИОИсполнителя ", + "name": "Наименование организации или ФИО исполнителя (в дательном падеже)", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "ПочтовыйАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ФИОВРодПад", + "name": "ФИО (в родительном падеже)", + "hint": "Ивановой Марии Дмитриевны", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "Телефон", + "name": "Телефон", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 15 + }, + { + "tag": "ЭлектроннаяПочта", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 50 + }, + { + "tag": "ДатаПосещенияСалона", + "name": "Дата посещения салона", + "hint": "ЧЧ.ММ.ГГГГ (10.02.2023)", + "group": 3, + "type": "date", + "length": 10 + }, + { + "tag": "НаименованиеСалонаКрасоты", + "name": "Наименование салона красоты", + "hint": "Монэ", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "НаименованиеОказаннойУслуги", + "name": "Наименование оказанной услуги", + "hint": "Окрашивание волос, маникюр", + "group": 3, + "type": "str40", + "length": 50 + }, + { + "tag": "СтоимостьУслуги", + "name": "Стоимость услуги", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 10 + }, + { + "tag": "ПодтверждающиеДокументы", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "Чеком об оплате", + "group": 3, + "type": "str40", + "length": 70 + }, + { + "tag": "ФИОМастера", + "name": "ФИО мастера", + "hint": "Петрова Дарья Леонидовна", + "group": 3, + "type": "fio", + "length": 40 + }, + { + "tag": "СутьПретензии", + "name": "Суть претензии", + "hint": "Цвет волос не соответствовал запрошенному и гарантированному парикмахером перед началом процедуры окрашивания", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "СтатусИсполненияОбязательств", + "name": "Статус исполнения обязательств", + "hint": "Не исполнены в установленный договором срок, исполнены частично", + "group": 4, + "type": "str40", + "length": 40 + }, + { + "tag": "ФИОЗаказчика", + "name": "ФИО заказчика", + "hint": "Иванова Мария Дмитриевна", + "group": 4, + "type": "fio", + "length": 40 + }, + { + "tag": "СуммаМатериальныхУбытков", + "name": "Сумма материальных убытков", + "hint": "В рублях, копейки через точку", + "group": 4, + "type": "currency", + "length": 12 + }, + { + "tag": "ОжидаемыйCрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ЧЧ.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 10 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "Вернуть сумму за оказанные услуги, возместить расходы", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ДокументыПрикладываемыеКПретензии", + "name": "Документы, прикладываемые к претензии", + "hint": "Копия чека об оплате", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "Дата", + "name": "Дата", + "hint": "ЧЧ.ММ.ГГГГ", + "group": 5, + "type": "date", + "length": 10 + }, + { + "tag": "ФИО", + "name": "ФИО Заказчика (в именительном падеже)", + "hint": "Иванова М.Д.", + "group": 5, + "type": "fio", + "length": 40 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + }, + { + "id": 5, + "name": "Подтверждение претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.docx" new file mode 100644 index 0000000..5631da2 Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.json" new file mode 100644 index 0000000..042e43b --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\260\321\200\321\203\321\210\320\265\320\275\320\270\320\270_\321\201\321\200\320\276\320\272\320\276\320\262_\320\276\320\272\320\260\320\267\320\260\320\275\320\270\321\217_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217_\321\200\320\260\320\261\320\276\321\202_tpl.json" @@ -0,0 +1,178 @@ +{ + "template": "претензия_о_нарушении_сроков_оказания_услуг_выполнения_работ_tpl.docx", + "name": "Претензия о нарушении сроков оказания услуг (выполнения работ)", + "deleted": "False", + "description": "Используйте этот документ, чтобы выразить протест по поводу задержки выполнения работ или оказания услуг и потребовать исправления ситуации.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаЗаключенияДоговора", + "name": "Дата заключения договора", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПерваяСторонаФИОтворПадеж", + "name": "Первая сторона (ФИО в творительном падеже)", + "hint": "Ивановым Алексеем Дмитриевичем", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "ВтораяСторонаФИОилиОргТворПадеж", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "ФИО или ООО \"Ромашка\"", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НомерДоговора", + "name": "Номер договора", + "hint": "№ 12345", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ОбязательствоИсполнителя", + "name": "Обязательство исполнителя", + "hint": "Обеспечить качество и своевременность оказания услуг", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СуммаОказанныхУслуг", + "name": "Стоимость услуг (работ)", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ПодтверждающиеДокументыТворительныйПадеж", + "name": "Подтверждающие документы", + "hint": "Квитанция или чек об оплате и т.д.", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаПодтверждающегоДокумента", + "name": "Дата подтверждающего документа", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПунктДоговораСуказанииемСрока", + "name": "Пункт договора, в котором указан срок оказания услуг (выполнения работ)", + "hint": "п. 4", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "СрокОказанияУслугВыполненияРабот", + "name": "Срок оказания услуг (выполнения работ)", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 100 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "Оказать услуги (выполнить работы) в новый установленный срок, вернуть сумму предварительной оплаты, возместить убытки, причиненные вследствие нарушения установленного срока передачи", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы, прикладываемые к претензии", + "hint": "Копия договора, копия квитанции об оплате и т.д.", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.docx" new file mode 100644 index 0000000..c4b5d59 Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.json" new file mode 100644 index 0000000..1e5a767 --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\275\320\265\320\264\320\276\321\201\321\202\320\260\321\202\320\272\320\260\321\205_\320\276\320\272\320\260\320\267\320\260\320\275\320\275\321\213\321\205_\321\203\321\201\320\273\321\203\320\263_\320\262\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\275\321\213\321\205_\321\200\320\260\320\261\320\276\321\202_tpl.json" @@ -0,0 +1,178 @@ +{ + "template": "претензия_о_недостатках_оказанных_услуг_выполненных_работ_tpl.docx", + "name": "Претензия о недостатках оказанных услуг (выполненных работ)", + "deleted": "False", + "description": "Этот шаблон поможет Вам официально выразить недовольство качеством полученных услуг и разрешить конфликты.", + "fields": [ + { + "tag": "АдресатОрганизация", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаЗаключенияДоговора", + "name": "Дата заключения договора", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПерваяСторонаФИОтворПадеж", + "name": "Первая сторона (ФИО в творительном падеже)", + "hint": "Ивановым Алексеем Дмитриевичем", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "ВтораяСторонаФИОилиОргТворПадеж", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "ФИО или ООО \"Ромашка\"", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "НомерДоговора", + "name": "Номер договора", + "hint": "№ 12345", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ОбязательствоИсполнителя", + "name": "Обязательство исполнителя", + "hint": "Обеспечить качество и своевременность оказания услуг", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СуммаОказанныхУслуг", + "name": "Стоимость услуг (работ)", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ПодтверждающиеДокументыТворительныйПадеж", + "name": "Подтверждающие документы", + "hint": "Квитанция или чек об оплате и т.д.", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаПодтверждающегоДокумента", + "name": "Дата подтверждающего документа", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ДатаОбнаруженияНедостатков", + "name": "Дата обнаружения недостатков", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 100 + }, + { + "tag": "НедостаткиОказанныхУслуг", + "name": "Недостатки оказанных услуг", + "hint": "Не соответствуют указанным в договоре требованиям", + "group": 3, + "type": "str", + "length": 250 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Безвозмездно устранить выявленные недостатки, возместить ущерб", + "hint": "Не соответствуют указанным в договоре требованиям", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы, прикладываемые к претензии", + "hint": "Копия договора, копия квитанции об оплате и т.д.", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" new file mode 100644 index 0000000..e5ffc8f Binary files /dev/null and "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.docx" differ diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" new file mode 100644 index 0000000..3762b5c --- /dev/null +++ "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\320\270/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_\320\276_\320\277\321\200\320\276\320\264\320\260\320\266\320\265_\321\202\320\276\320\262\320\260\321\200\320\260_\320\275\320\265\320\275\320\260\320\264\320\273\320\265\320\266\320\260\321\211\320\265\320\263\320\276_\320\272\320\260\321\207\320\265\321\201\321\202\320\262\320\260_tpl.json" @@ -0,0 +1,170 @@ +{ + "template": "претензия_о_продаже_товара_ненадлежащего_качества_tpl.docx", + "name": "Претензия о продаже товара ненадлежащего качества", + "deleted": "False", + "description": "Этот шаблон позволит вам запросить возврат или обмен товара, который оказался низкого качества, и защитить ваши права как потребителя.", + "fields": [ + { + "tag": "АдресатОрганизацияИлиФИО", + "name": "Наименование организации", + "hint": "ООО \"Ромашка\"", + "group": 1, + "type": "str40", + "length": 40 + }, + { + "tag": "АдресатАдрес", + "name": "Почтовый адрес", + "hint": "633104, Новосибирская обл., г. Обь, проспект Мозжерина, д. 10 офис 201", + "group": 1, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительФИОродПадеж", + "name": "ФИО (в родительном падеже)", + "hint": "Иванова Алексея Дмитриевича", + "group": 2, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительАдрес", + "name": "Почтовый адрес", + "hint": "450045, Респ. Башкортостан, г. Уфа, ул. Ленина, 36, кв. 60", + "group": 2, + "type": "str", + "length": 100 + }, + { + "tag": "ОтправительТелефон", + "name": "Номер телефона", + "hint": "+79175678356", + "group": 2, + "type": "phone", + "length": 12 + }, + { + "tag": "ОтправительEmail", + "name": "Электронная почта", + "hint": "shablonizator@mail.ru", + "group": 2, + "type": "email", + "length": 40 + }, + { + "tag": "ДатаЗаключенияДоговора", + "name": "Дата заключения договора", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ПерваяСторонаФИОтворПадеж", + "name": "Первая сторона (ФИО в творительном падеже)", + "hint": "Ивановым Алексеем Дмитриевичем", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "ВтораяСторонаФИОилиОргТворПадеж", + "name": "Подтверждающие документы (в творительном падеже)", + "hint": "ФИО или ООО \"Ромашка\"", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ХарактеристикаТовара", + "name": "Характеристика товара", + "hint": "наименование, артикул", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СтоимостьТовара", + "name": "Стоимость услуг (работ)", + "hint": "В рублях, копейки через точку", + "group": 3, + "type": "currency", + "length": 100 + }, + { + "tag": "ПодтверждающиеДокументыТворительныйПадеж", + "name": "Подтверждающие документы (в «творительном падеже)", + "hint": "квитанцией, чеком об оплате", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "ДатаПодтверждающегоДокумента", + "name": "Дата подтверждающего документа", + "hint": "ДД.ММ.ГГГГ", + "group": 3, + "type": "date", + "length": 40 + }, + { + "tag": "ГарантийныйСрок", + "name": "Гарантийный срок товара", + "hint": "Укажите количество дней", + "group": 3, + "type": "int", + "length": 100 + }, + { + "tag": "НедостаткиТовара", + "name": "Недостатки товара", + "hint": "У товара присутствует брак, невозможно использовать по назначению из-за неисправности", + "group": 3, + "type": "str", + "length": 100 + }, + { + "tag": "СодержаниеПредъявляемыхТребований", + "name": "Содержание предъявляемых требований", + "hint": "замены на такой же товар, возместить убытки в полном объеме", + "group": 4, + "type": "str", + "length": 100 + }, + { + "tag": "ОжидаемыйСрокИсполненияТребований", + "name": "Ожидаемый срок исполнения требований", + "hint": "ДД.ММ.ГГГГ", + "group": 4, + "type": "date", + "length": 40 + }, + { + "tag": "Документы", + "name": "Документы, прикладываемые к претензии", + "hint": "Копия договора, копия квитанции об оплате и т.д.", + "group": 4, + "type": "str", + "length": 100 + } + ], + "groups": [ + { + "id": 1, + "name": "Адресат" + }, + { + "id": 2, + "name": "Отправитель" + }, + { + "id": 3, + "name": "Данные о договоре" + }, + { + "id": 4, + "name": "Содержание претензии" + } + ] +} \ No newline at end of file diff --git "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" "b/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" deleted file mode 100644 index 50a4a2d..0000000 Binary files "a/backend/data/\320\277\321\200\320\265\321\202\320\265\320\275\320\267\320\270\321\217_tpl.docx" and /dev/null differ diff --git a/backend/documents/admin.py b/backend/documents/admin.py index 8ca55db..4a90842 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -1,4 +1,5 @@ """Настройки админки для приложения "Документы".""" +from django import forms from django.contrib import admin from documents import models @@ -36,6 +37,12 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): ) return super().formfield_for_foreignkey(db_field, request, **kwargs) + def formfield_for_dbfield(self, db_field, request, **kwargs): + formfield = super().formfield_for_dbfield(db_field, request, **kwargs) + if db_field.name == "default": + formfield.strip = False + return formfield + @admin.register(models.Template) class TemplateAdmin(admin.ModelAdmin): @@ -44,13 +51,13 @@ class TemplateAdmin(admin.ModelAdmin): "owner", "category", "template", - "modified", + "updated", "deleted", "description", "image", ) list_filter = ("owner", "category", "deleted") - readonly_fields = ("id",) + readonly_fields = ("id", "updated") inlines = (TemplateFieldInlineAdmin,) def get_form(self, request, instance=None, **kwargs): @@ -60,7 +67,7 @@ def get_form(self, request, instance=None, **kwargs): @admin.register(models.TemplateFieldGroup) class TemplateFieldGroupAdmin(admin.ModelAdmin): - list_display = ("id", "name", "template") + list_display = ("id", "name", "template",) readonly_fields = ("id",) search_fields = ("name", "template") @@ -72,6 +79,17 @@ class TemplateFieldTypeAdmin(admin.ModelAdmin): search_fields = ("name",) +class TemplateFieldForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # allow space for default value + self.fields["default"].strip = False + + class Meta: + model = models.TemplateField + fields = "__all__" + + @admin.register(models.TemplateField) class TemplateFieldAdmin(admin.ModelAdmin): list_display = ( @@ -83,11 +101,14 @@ class TemplateFieldAdmin(admin.ModelAdmin): "group", "type", "length", + # "base_object_field", ) list_filter = ("template",) readonly_fields = ("id",) search_fields = ("name",) + form = TemplateFieldForm + def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "group": field_id = request.resolver_match.kwargs.get("object_id") diff --git a/backend/documents/management/commands/render.py b/backend/documents/management/commands/render.py index e69de29..8b13789 100644 --- a/backend/documents/management/commands/render.py +++ b/backend/documents/management/commands/render.py @@ -0,0 +1 @@ + diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py index 8cada4c..e1a5447 100644 --- a/backend/documents/migrations/0001_initial.py +++ b/backend/documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2 on 2023-10-07 11:36 +# Generated by Django 3.2 on 2024-04-04 08:47 from django.conf import settings from django.db import migrations, models @@ -18,59 +18,151 @@ class Migration(migrations.Migration): name='Category', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), + ('name', models.CharField(max_length=255, verbose_name='Наименование категории')), ], + options={ + 'verbose_name': 'Категория', + 'verbose_name_plural': 'Категории', + 'ordering': ('name',), + }, ), migrations.CreateModel( name='Document', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('completed', models.BooleanField()), - ('description', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('completed', models.BooleanField(default=False, verbose_name='Документ заполнен')), + ('description', models.TextField(verbose_name='Описание документа')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to=settings.AUTH_USER_MODEL, verbose_name='Автор документа')), ], + options={ + 'verbose_name': 'Документ', + 'verbose_name_plural': 'Документы', + 'ordering': ('created',), + 'default_related_name': 'documents', + }, ), migrations.CreateModel( name='Template', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('template', models.FileField(upload_to='templates/')), - ('name', models.CharField(max_length=255)), - ('modified', models.DateField()), - ('deleted', models.BooleanField()), - ('description', models.TextField()), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.category')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('template', models.FileField(upload_to='templates/', verbose_name='Файл шаблона')), + ('name', models.CharField(max_length=255, verbose_name='Наименование шаблона')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('deleted', models.BooleanField(verbose_name='Удален')), + ('description', models.TextField(verbose_name='Описание шаблона')), + ('image', models.ImageField(blank=True, null=True, upload_to='posts/', verbose_name='Картинка')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='templates', to='documents.category', verbose_name='Категория')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='templates', to=settings.AUTH_USER_MODEL, verbose_name='Автор шаблона')), ], + options={ + 'verbose_name': 'Шаблон', + 'verbose_name_plural': 'Шаблоны', + 'ordering': ('name',), + 'default_related_name': 'templates', + }, + ), + migrations.CreateModel( + name='TemplateFieldType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.SlugField(unique=True, verbose_name='Тип данных')), + ('name', models.CharField(max_length=50, verbose_name='Наименование типа')), + ('mask', models.CharField(blank=True, max_length=255, verbose_name='Маска допустимых значений')), + ], + options={ + 'verbose_name': 'Тип поля шаблона', + 'verbose_name_plural': 'Типы поля шаблона', + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='TemplateFieldGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Наименование группы полей')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_groups', to='documents.template', verbose_name='Шаблон')), + ], + options={ + 'verbose_name': 'Группа полей', + 'verbose_name_plural': 'Группы полей', + 'ordering': ('id',), + }, ), migrations.CreateModel( name='TemplateField', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('tag', models.CharField(max_length=255)), - ('name', models.CharField(max_length=255)), - ('hint', models.TextField()), - ('template_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.template')), + ('tag', models.CharField(max_length=255, verbose_name='Тэг поля')), + ('name', models.CharField(max_length=255, verbose_name='Наименование поля')), + ('hint', models.CharField(blank=True, max_length=255, verbose_name='Подсказка')), + ('length', models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер поля ввода')), + ('default', models.CharField(blank=True, max_length=255, null=True, verbose_name='Значение по умолчанию')), + ('group', models.ForeignKey(blank=True, help_text='Группа полей в шаблоне', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fields', to='documents.templatefieldgroup', verbose_name='Группа')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='documents.template', verbose_name='Шаблон')), + ('type', models.ForeignKey(blank=True, help_text='Тип поля', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fields', to='documents.templatefieldtype', verbose_name='Тип')), ], + options={ + 'verbose_name': 'Поле шаблона', + 'verbose_name_plural': 'Поля шаблона', + 'ordering': ('template', 'name'), + 'default_related_name': 'fields', + }, + ), + migrations.CreateModel( + name='FavTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_templates', to='documents.template', verbose_name='Шаблон')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_templates', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Избранный шаблон', + 'verbose_name_plural': 'Избранные шаблоны', + 'ordering': ('user', 'template'), + 'default_related_name': 'favorite_templates', + }, + ), + migrations.CreateModel( + name='FavDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to='documents.document', verbose_name='Документ')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Избранный документ', + 'verbose_name_plural': 'Избранные документы', + 'ordering': ('user', 'document'), + 'default_related_name': 'favorite_documents', + }, ), migrations.CreateModel( name='DocumentField', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.CharField(max_length=255)), - ('description', models.TextField()), - ('document_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.document')), - ('field_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.templatefield')), + ('value', models.CharField(max_length=255, verbose_name='Содержимое поля')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='document_fields', to='documents.document', verbose_name='Документ')), + ('field', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='document_fields', to='documents.templatefield', verbose_name='Поле')), ], + options={ + 'verbose_name': 'Поле документа', + 'verbose_name_plural': 'Поля документа', + 'ordering': ('field__template', 'field'), + }, ), migrations.AddField( model_name='document', - name='template_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.template'), + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='documents', to='documents.template', verbose_name='Шаблон'), ), - migrations.AddField( - model_name='document', - name='user_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + migrations.AddConstraint( + model_name='favtemplate', + constraint=models.UniqueConstraint(fields=('user', 'template'), name='unique_user_template'), + ), + migrations.AddConstraint( + model_name='favdocument', + constraint=models.UniqueConstraint(fields=('user', 'document'), name='unique_user_document'), ), ] diff --git a/backend/documents/migrations/0002_auto_20231007_1448.py b/backend/documents/migrations/0002_auto_20231007_1448.py deleted file mode 100644 index cb9201d..0000000 --- a/backend/documents/migrations/0002_auto_20231007_1448.py +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by Django 3.2 on 2023-10-07 11:48 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='category', - options={'ordering': ('name',), 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'}, - ), - migrations.AlterModelOptions( - name='document', - options={'ordering': ('created',), 'verbose_name': 'Документ', 'verbose_name_plural': 'Документы'}, - ), - migrations.AlterModelOptions( - name='documentfield', - options={'verbose_name': 'Поле документа', 'verbose_name_plural': 'Поля документа'}, - ), - migrations.AlterModelOptions( - name='template', - options={'ordering': ('name',), 'verbose_name': 'Шаблон', 'verbose_name_plural': 'Шаблоны'}, - ), - migrations.AlterModelOptions( - name='templatefield', - options={'ordering': ('name',), 'verbose_name': 'Поле шаблона', 'verbose_name_plural': 'Поля шаблона'}, - ), - migrations.AlterField( - model_name='category', - name='name', - field=models.CharField(max_length=255, verbose_name='Наименование категории'), - ), - migrations.AlterField( - model_name='document', - name='completed', - field=models.BooleanField(verbose_name='Статус документа'), - ), - migrations.AlterField( - model_name='document', - name='created', - field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), - ), - migrations.AlterField( - model_name='document', - name='description', - field=models.TextField(verbose_name='Описание документа'), - ), - migrations.AlterField( - model_name='documentfield', - name='description', - field=models.TextField(verbose_name='Описание поля'), - ), - migrations.AlterField( - model_name='documentfield', - name='value', - field=models.CharField(max_length=255, verbose_name='Содержимое поля'), - ), - migrations.AlterField( - model_name='template', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.category', verbose_name='Категория'), - ), - migrations.AlterField( - model_name='template', - name='description', - field=models.TextField(verbose_name='Описание шаблона'), - ), - migrations.AlterField( - model_name='template', - name='name', - field=models.CharField(max_length=255, verbose_name='Наименование шаблона'), - ), - migrations.AlterField( - model_name='template', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор шаблона'), - ), - migrations.AlterField( - model_name='templatefield', - name='hint', - field=models.TextField(verbose_name='Подсказка'), - ), - migrations.AlterField( - model_name='templatefield', - name='name', - field=models.CharField(max_length=255, verbose_name='Наименование поля'), - ), - migrations.AlterField( - model_name='templatefield', - name='tag', - field=models.CharField(max_length=255, verbose_name='Тэг поля'), - ), - ] diff --git a/backend/documents/migrations/0003_auto_20231007_1516.py b/backend/documents/migrations/0003_auto_20231007_1516.py deleted file mode 100644 index ff2acb8..0000000 --- a/backend/documents/migrations/0003_auto_20231007_1516.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 3.2 on 2023-10-07 12:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0002_auto_20231007_1448'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='template_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.template', verbose_name='Шаблон'), - ), - migrations.AlterField( - model_name='document', - name='user_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор документа'), - ), - migrations.AlterField( - model_name='documentfield', - name='document_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.document', verbose_name='Документ'), - ), - migrations.AlterField( - model_name='documentfield', - name='field_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.templatefield', verbose_name='Поле'), - ), - migrations.AlterField( - model_name='template', - name='deleted', - field=models.BooleanField(verbose_name='Удален'), - ), - migrations.AlterField( - model_name='template', - name='modified', - field=models.DateField(verbose_name='Дата модификации'), - ), - migrations.AlterField( - model_name='templatefield', - name='template_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.template', verbose_name='Шаблон'), - ), - ] diff --git a/backend/documents/migrations/0004_alter_templatefield_hint.py b/backend/documents/migrations/0004_alter_templatefield_hint.py deleted file mode 100644 index 7bd7d91..0000000 --- a/backend/documents/migrations/0004_alter_templatefield_hint.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-07 12:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0003_auto_20231007_1516'), - ] - - operations = [ - migrations.AlterField( - model_name='templatefield', - name='hint', - field=models.TextField(blank=True, null=True, verbose_name='Подсказка'), - ), - ] diff --git a/backend/documents/migrations/0005_alter_document_completed.py b/backend/documents/migrations/0005_alter_document_completed.py deleted file mode 100644 index d2aa450..0000000 --- a/backend/documents/migrations/0005_alter_document_completed.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-07 12:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0004_alter_templatefield_hint'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='completed', - field=models.BooleanField(verbose_name='Документ заполнен'), - ), - ] diff --git a/backend/documents/migrations/0006_alter_templatefield_template_id.py b/backend/documents/migrations/0006_alter_templatefield_template_id.py deleted file mode 100644 index 119cec5..0000000 --- a/backend/documents/migrations/0006_alter_templatefield_template_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 08:55 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0005_alter_document_completed'), - ] - - operations = [ - migrations.AlterField( - model_name='templatefield', - name='template_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='documents.template', verbose_name='Шаблон'), - ), - ] diff --git a/backend/documents/migrations/0006_auto_20231011_1727.py b/backend/documents/migrations/0006_auto_20231011_1727.py deleted file mode 100644 index dda8c65..0000000 --- a/backend/documents/migrations/0006_auto_20231011_1727.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 14:27 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0005_alter_document_completed'), - ] - - operations = [ - migrations.RenameField( - model_name='document', - old_name='user_id', - new_name='owner', - ), - migrations.RemoveField( - model_name='documentfield', - name='document_id', - ), - migrations.CreateModel( - name='FieldToDocument', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.document')), - ('fields', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.documentfield')), - ], - options={ - 'verbose_name': 'Связь между полем и документом', - 'verbose_name_plural': 'Связи между полями и документами', - }, - ), - ] diff --git a/backend/documents/migrations/0007_auto_20231011_2320.py b/backend/documents/migrations/0007_auto_20231011_2320.py deleted file mode 100644 index 9aa93af..0000000 --- a/backend/documents/migrations/0007_auto_20231011_2320.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 20:20 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0006_alter_templatefield_template_id'), - ] - - operations = [ - migrations.AlterField( - model_name='template', - name='category', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.category', verbose_name='Категория'), - ), - migrations.AlterField( - model_name='template', - name='modified', - field=models.DateField(auto_now=True, verbose_name='Дата модификации'), - ), - migrations.AlterField( - model_name='template', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Автор шаблона'), - ), - ] diff --git a/backend/documents/migrations/0007_document_fields.py b/backend/documents/migrations/0007_document_fields.py deleted file mode 100644 index 07267fe..0000000 --- a/backend/documents/migrations/0007_document_fields.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 14:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0006_auto_20231011_1727'), - ] - - operations = [ - migrations.AddField( - model_name='document', - name='fields', - field=models.ManyToManyField(through='documents.FieldToDocument', to='documents.DocumentField'), - ), - ] diff --git a/backend/documents/migrations/0008_rename_fields_document_docuemnt_fields.py b/backend/documents/migrations/0008_rename_fields_document_docuemnt_fields.py deleted file mode 100644 index 2dde33f..0000000 --- a/backend/documents/migrations/0008_rename_fields_document_docuemnt_fields.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 14:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0007_document_fields'), - ] - - operations = [ - migrations.RenameField( - model_name='document', - old_name='fields', - new_name='docuemnt_fields', - ), - ] diff --git a/backend/documents/migrations/0009_rename_docuemnt_fields_document_document_fields.py b/backend/documents/migrations/0009_rename_docuemnt_fields_document_document_fields.py deleted file mode 100644 index 6e02d5e..0000000 --- a/backend/documents/migrations/0009_rename_docuemnt_fields_document_document_fields.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-11 14:43 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0008_rename_fields_document_docuemnt_fields'), - ] - - operations = [ - migrations.RenameField( - model_name='document', - old_name='docuemnt_fields', - new_name='document_fields', - ), - ] diff --git a/backend/documents/migrations/0010_auto_20231014_1451.py b/backend/documents/migrations/0010_auto_20231014_1451.py deleted file mode 100644 index a56fec3..0000000 --- a/backend/documents/migrations/0010_auto_20231014_1451.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.2 on 2023-10-14 11:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0009_rename_docuemnt_fields_document_document_fields'), - ] - - operations = [ - migrations.RenameField( - model_name='document', - old_name='template_id', - new_name='template', - ), - migrations.RenameField( - model_name='documentfield', - old_name='field_id', - new_name='field', - ), - migrations.AlterField( - model_name='documentfield', - name='description', - field=models.TextField(default='Нет описания', verbose_name='Описание поля'), - ), - ] diff --git a/backend/documents/migrations/0011_merge_0007_auto_20231011_2320_0010_auto_20231014_1451.py b/backend/documents/migrations/0011_merge_0007_auto_20231011_2320_0010_auto_20231014_1451.py deleted file mode 100644 index 846b7aa..0000000 --- a/backend/documents/migrations/0011_merge_0007_auto_20231011_2320_0010_auto_20231014_1451.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.2 on 2023-10-14 13:28 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0007_auto_20231011_2320'), - ('documents', '0010_auto_20231014_1451'), - ] - - operations = [ - ] diff --git a/backend/documents/migrations/0012_auto_20231014_2014.py b/backend/documents/migrations/0012_auto_20231014_2014.py deleted file mode 100644 index 9a79684..0000000 --- a/backend/documents/migrations/0012_auto_20231014_2014.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 3.2 on 2023-10-14 17:14 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0011_merge_0007_auto_20231011_2320_0010_auto_20231014_1451'), - ] - - operations = [ - migrations.AlterField( - model_name='documentfield', - name='description', - field=models.TextField(verbose_name='Описание поля'), - ), - migrations.CreateModel( - name='FavTemplate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('template', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='documents.template', verbose_name='Шаблон')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), - ], - options={ - 'verbose_name': 'Избранный шаблон', - 'verbose_name_plural': 'Избранные шаблоны', - }, - ), - migrations.CreateModel( - name='FavDocument', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('document', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='documents.document', verbose_name='Документ')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), - ], - options={ - 'verbose_name': 'Избранный документ', - 'verbose_name_plural': 'Избранные документы', - }, - ), - ] diff --git a/backend/documents/migrations/0013_auto_20231014_2225.py b/backend/documents/migrations/0013_auto_20231014_2225.py deleted file mode 100644 index b04bb26..0000000 --- a/backend/documents/migrations/0013_auto_20231014_2225.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.2 on 2023-10-14 19:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0012_auto_20231014_2014'), - ] - - operations = [ - migrations.RenameField( - model_name='templatefield', - old_name='template_id', - new_name='template', - ), - migrations.CreateModel( - name='TemplateFieldGroup', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='Наименование группы полей')), - ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_groups', to='documents.template', verbose_name='Шаблон')), - ], - options={ - 'verbose_name': 'Группа полей', - 'verbose_name_plural': 'Группы полей', - 'ordering': ('id',), - }, - ), - migrations.AddField( - model_name='templatefield', - name='group', - field=models.ForeignKey(blank=True, help_text='Группа полей в шаблоне', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fields', to='documents.templatefieldgroup', verbose_name='Группа'), - ), - ] diff --git a/backend/documents/migrations/0014_auto_20231014_2357.py b/backend/documents/migrations/0014_auto_20231014_2357.py deleted file mode 100644 index 0c919a4..0000000 --- a/backend/documents/migrations/0014_auto_20231014_2357.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2 on 2023-10-14 20:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0013_auto_20231014_2225'), - ] - - operations = [ - migrations.AddConstraint( - model_name='favdocument', - constraint=models.UniqueConstraint(fields=('user', 'document'), name='unique_user_document'), - ), - migrations.AddConstraint( - model_name='favtemplate', - constraint=models.UniqueConstraint(fields=('user', 'template'), name='unique_user_template'), - ), - ] diff --git a/backend/documents/migrations/0015_auto_20231017_2347.py b/backend/documents/migrations/0015_auto_20231017_2347.py deleted file mode 100644 index 20e1e25..0000000 --- a/backend/documents/migrations/0015_auto_20231017_2347.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2 on 2023-10-17 20:47 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0014_auto_20231014_2357'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='documents.template', verbose_name='Шаблон'), - ), - migrations.AlterField( - model_name='documentfield', - name='field', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='documents.templatefield', verbose_name='Поле'), - ), - ] diff --git a/backend/documents/migrations/0016_auto_20231018_0034.py b/backend/documents/migrations/0016_auto_20231018_0034.py deleted file mode 100644 index ee37920..0000000 --- a/backend/documents/migrations/0016_auto_20231018_0034.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 3.2 on 2023-10-17 21:34 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0015_auto_20231017_2347'), - ] - - operations = [ - migrations.AlterField( - model_name='template', - name='category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.category', verbose_name='Категория'), - ), - migrations.AlterField( - model_name='template', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Автор шаблона'), - ), - ] diff --git a/backend/documents/migrations/0017_auto_20231019_1108.py b/backend/documents/migrations/0017_auto_20231019_1108.py deleted file mode 100644 index d031ca7..0000000 --- a/backend/documents/migrations/0017_auto_20231019_1108.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 3.2 on 2023-10-19 08:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0016_auto_20231018_0034'), - ] - - operations = [ - migrations.AlterModelOptions( - name='document', - options={'default_related_name': 'documents', 'ordering': ('created',), 'verbose_name': 'Документ', 'verbose_name_plural': 'Документы'}, - ), - migrations.AddField( - model_name='document', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'), - ), - migrations.AlterField( - model_name='document', - name='document_fields', - field=models.ManyToManyField(related_name='documents', through='documents.FieldToDocument', to='documents.DocumentField'), - ), - migrations.AlterField( - model_name='document', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to=settings.AUTH_USER_MODEL, verbose_name='Автор документа'), - ), - migrations.AlterField( - model_name='document', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='documents', to='documents.template', verbose_name='Шаблон'), - ), - ] diff --git a/backend/documents/migrations/0018_alter_documentfield_description.py b/backend/documents/migrations/0018_alter_documentfield_description.py deleted file mode 100644 index cd10473..0000000 --- a/backend/documents/migrations/0018_alter_documentfield_description.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-19 14:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0017_auto_20231019_1108'), - ] - - operations = [ - migrations.AlterField( - model_name='documentfield', - name='description', - field=models.TextField(blank=True, null=True, verbose_name='Описание поля'), - ), - ] diff --git a/backend/documents/migrations/0019_auto_20231023_2010.py b/backend/documents/migrations/0019_auto_20231023_2010.py deleted file mode 100644 index 7c415e0..0000000 --- a/backend/documents/migrations/0019_auto_20231023_2010.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2 on 2023-10-23 17:10 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0018_alter_documentfield_description'), - ] - - operations = [ - migrations.CreateModel( - name='TemplateFieldType', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(unique=True, verbose_name='Тип данных')), - ('name', models.CharField(max_length=50, verbose_name='Наименование типа')), - ('mask', models.CharField(blank=True, max_length=255, null=True, verbose_name='Маска допустимых значений')), - ], - options={ - 'verbose_name': 'Тип поля шаблона', - 'verbose_name_plural': 'Типы поля шаблона', - 'ordering': ('name',), - }, - ), - migrations.AddField( - model_name='templatefield', - name='type', - field=models.ForeignKey(blank=True, help_text='Тип поля', null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.templatefieldtype', verbose_name='Тип'), - ), - ] diff --git a/backend/documents/migrations/0020_alter_templatefield_hint.py b/backend/documents/migrations/0020_alter_templatefield_hint.py deleted file mode 100644 index ee66800..0000000 --- a/backend/documents/migrations/0020_alter_templatefield_hint.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-23 17:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0019_auto_20231023_2010'), - ] - - operations = [ - migrations.AlterField( - model_name='templatefield', - name='hint', - field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Подсказка'), - ), - ] diff --git a/backend/documents/migrations/0021_rename_slug_templatefieldtype_type.py b/backend/documents/migrations/0021_rename_slug_templatefieldtype_type.py deleted file mode 100644 index 60be64a..0000000 --- a/backend/documents/migrations/0021_rename_slug_templatefieldtype_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-10-23 18:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0020_alter_templatefield_hint'), - ] - - operations = [ - migrations.RenameField( - model_name='templatefieldtype', - old_name='slug', - new_name='type', - ), - ] diff --git a/backend/documents/migrations/0022_auto_20231025_2055.py b/backend/documents/migrations/0022_auto_20231025_2055.py deleted file mode 100644 index ff58211..0000000 --- a/backend/documents/migrations/0022_auto_20231025_2055.py +++ /dev/null @@ -1,107 +0,0 @@ -# Generated by Django 3.2 on 2023-10-25 17:55 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('documents', '0021_rename_slug_templatefieldtype_type'), - ] - - operations = [ - migrations.AlterModelOptions( - name='documentfield', - options={'ordering': ('field__template', 'field'), 'verbose_name': 'Поле документа', 'verbose_name_plural': 'Поля документа'}, - ), - migrations.AlterModelOptions( - name='favdocument', - options={'default_related_name': 'favorite_documents', 'ordering': ('user', 'document'), 'verbose_name': 'Избранный документ', 'verbose_name_plural': 'Избранные документы'}, - ), - migrations.AlterModelOptions( - name='favtemplate', - options={'default_related_name': 'favorite_templates', 'ordering': ('user', 'template'), 'verbose_name': 'Избранный шаблон', 'verbose_name_plural': 'Избранные шаблоны'}, - ), - migrations.AlterModelOptions( - name='template', - options={'default_related_name': 'templates', 'ordering': ('name',), 'verbose_name': 'Шаблон', 'verbose_name_plural': 'Шаблоны'}, - ), - migrations.AlterModelOptions( - name='templatefield', - options={'default_related_name': 'fields', 'ordering': ('template', 'name'), 'verbose_name': 'Поле шаблона', 'verbose_name_plural': 'Поля шаблона'}, - ), - migrations.AlterField( - model_name='document', - name='completed', - field=models.BooleanField(default=False, verbose_name='Документ заполнен'), - ), - migrations.AlterField( - model_name='documentfield', - name='description', - field=models.TextField(blank=True, default='', verbose_name='Описание поля'), - preserve_default=False, - ), - migrations.AlterField( - model_name='documentfield', - name='field', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='document_fields', to='documents.templatefield', verbose_name='Поле'), - ), - migrations.AlterField( - model_name='favdocument', - name='document', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to='documents.document', verbose_name='Документ'), - preserve_default=False, - ), - migrations.AlterField( - model_name='favdocument', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to='users.user', verbose_name='Пользователь'), - preserve_default=False, - ), - migrations.AlterField( - model_name='favtemplate', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_templates', to='documents.template', verbose_name='Шаблон'), - preserve_default=False, - ), - migrations.AlterField( - model_name='favtemplate', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_templates', to='users.user', verbose_name='Пользователь'), - preserve_default=False, - ), - migrations.AlterField( - model_name='fieldtodocument', - name='document', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='document_of_field', to='documents.document'), - ), - migrations.AlterField( - model_name='fieldtodocument', - name='fields', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields_of_document', to='documents.documentfield'), - ), - migrations.AlterField( - model_name='template', - name='category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='templates', to='documents.category', verbose_name='Категория'), - ), - migrations.AlterField( - model_name='template', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='templates', to=settings.AUTH_USER_MODEL, verbose_name='Автор шаблона'), - ), - migrations.AlterField( - model_name='templatefield', - name='hint', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='Подсказка'), - preserve_default=False, - ), - migrations.AlterField( - model_name='templatefield', - name='type', - field=models.ForeignKey(blank=True, help_text='Тип поля', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fields', to='documents.templatefieldtype', verbose_name='Тип'), - ), - ] diff --git a/backend/documents/migrations/0023_alter_templatefieldtype_mask.py b/backend/documents/migrations/0023_alter_templatefieldtype_mask.py deleted file mode 100644 index c20b6e3..0000000 --- a/backend/documents/migrations/0023_alter_templatefieldtype_mask.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2 on 2023-10-25 18:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0022_auto_20231025_2055'), - ] - - operations = [ - migrations.AlterField( - model_name='templatefieldtype', - name='mask', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='Маска допустимых значений'), - preserve_default=False, - ), - ] diff --git a/backend/documents/migrations/0024_templatefield_length.py b/backend/documents/migrations/0024_templatefield_length.py deleted file mode 100644 index 4bd25e7..0000000 --- a/backend/documents/migrations/0024_templatefield_length.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-11-03 14:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0023_alter_templatefieldtype_mask'), - ] - - operations = [ - migrations.AddField( - model_name='templatefield', - name='length', - field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер поля ввода'), - ), - ] diff --git a/backend/documents/migrations/0025_templatefield_image.py b/backend/documents/migrations/0025_templatefield_image.py deleted file mode 100644 index d2c86af..0000000 --- a/backend/documents/migrations/0025_templatefield_image.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2023-11-07 10:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0024_templatefield_length'), - ] - - operations = [ - migrations.AddField( - model_name='templatefield', - name='image', - field=models.ImageField(blank=True, null=True, upload_to='posts/', verbose_name='Картинка'), - ), - ] diff --git a/backend/documents/migrations/0026_auto_20231107_1326.py b/backend/documents/migrations/0026_auto_20231107_1326.py deleted file mode 100644 index b5fddab..0000000 --- a/backend/documents/migrations/0026_auto_20231107_1326.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.2 on 2023-11-07 10:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0025_templatefield_image'), - ] - - operations = [ - migrations.RemoveField( - model_name='templatefield', - name='image', - ), - migrations.AddField( - model_name='template', - name='image', - field=models.ImageField(blank=True, null=True, upload_to='posts/', verbose_name='Картинка'), - ), - ] diff --git a/backend/documents/migrations/0027_auto_20231108_1053.py b/backend/documents/migrations/0027_auto_20231108_1053.py deleted file mode 100644 index 151d890..0000000 --- a/backend/documents/migrations/0027_auto_20231108_1053.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.2 on 2023-11-08 07:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0026_auto_20231107_1326'), - ] - - operations = [ - migrations.RemoveField( - model_name='document', - name='document_fields', - ), - migrations.AddField( - model_name='documentfield', - name='document', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='document_fields', to='documents.document', verbose_name='Документ'), - preserve_default=False, - ), - migrations.DeleteModel( - name='FieldToDocument', - ), - ] diff --git a/backend/documents/migrations/0028_remove_documentfield_description.py b/backend/documents/migrations/0028_remove_documentfield_description.py deleted file mode 100644 index 5e0e3bc..0000000 --- a/backend/documents/migrations/0028_remove_documentfield_description.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2 on 2023-11-08 08:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('documents', '0027_auto_20231108_1053'), - ] - - operations = [ - migrations.RemoveField( - model_name='documentfield', - name='description', - ), - ] diff --git a/backend/documents/models.py b/backend/documents/models.py index f90dffe..75c3de9 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -1,9 +1,16 @@ """Модели документов.""" +from typing import List, Tuple + from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models from core.constants import Messages +from core.template_render import DocumentTemplate +# from base_objects.models import ( +# BaseObject, +# BaseObjectField, +# ) User = get_user_model() @@ -43,11 +50,15 @@ class Template(models.Model): null=True, blank=True, ) - template = models.FileField(upload_to="templates/") + template = models.FileField( + upload_to="templates/", verbose_name="Файл шаблона" + ) name = models.CharField( max_length=255, verbose_name="Наименование шаблона" ) - modified = models.DateField(verbose_name="Дата модификации", auto_now=True) + updated = models.DateTimeField( + verbose_name="Дата изменения", auto_now=True + ) deleted = models.BooleanField(verbose_name="Удален") description = models.TextField(verbose_name="Описание шаблона") image = models.ImageField( @@ -79,6 +90,45 @@ def save(self, *args, **kwargs): print(e) return super().save(*args, **kwargs) + def get_inconsistent_tags(self) -> Tuple[Tuple, Tuple]: + """ + Возвращает списки несогласованных тэгов между БД и шаблоном docx. + + :returns: (excess_tags, excess_fields) + excess_tags - кортеж тэгов, которые имеются в docx, но отсутствуют в БД. + excess_fields - кортеж тэгов, которые имеются в БД, но отсутствуют в docx. + """ + docx_tags, field_tags = set(), set() + if self.template: + try: + doc = DocumentTemplate(self.template) + docx_tags = set(doc.get_tags()) + except Exception as e: + print(e) # TODO: add logging + + field_tags = {field.tag for field in self.fields.all()} + excess_tags = tuple(docx_tags - field_tags) + excess_fields = tuple(field_tags - docx_tags) + return (excess_tags, excess_fields) + + def get_consistency_errors(self) -> List: + """Генерирует ответ в зависимости от согласованности полей шаблона.""" + + excess_tags, excess_fields = self.get_inconsistent_tags() + errors = [] + if excess_tags: + errors.append( + {"message": Messages.TEMPLATE_EXCESS_TAGS, "tags": excess_tags} + ) + if excess_fields: + errors.append( + { + "message": Messages.TEMPLATE_EXCESS_FIELDS, + "tags": excess_fields, + } + ) + return errors + class TemplateFieldGroup(models.Model): """Группы полей шаблона.""" @@ -94,6 +144,15 @@ class TemplateFieldGroup(models.Model): verbose_name="Наименование группы полей", ) + # type_object = models.ForeignKey( + # BaseObject, + # on_delete=models.SET_NULL, + # verbose_name="Обьект", + # null=True, + # blank=True, + # # default=None + # ) + class Meta: verbose_name = "Группа полей" verbose_name_plural = "Группы полей" @@ -133,6 +192,13 @@ class TemplateField(models.Model): on_delete=models.CASCADE, verbose_name="Шаблон", ) + # base_object_field = models.ForeignKey( + # BaseObjectField, + # on_delete=models.SET_NULL, + # verbose_name="Поле базового обьекта", + # null=True, + # blank=True, + # ) tag = models.CharField(max_length=255, verbose_name="Тэг поля") name = models.CharField(max_length=255, verbose_name="Наименование поля") hint = models.CharField( @@ -157,6 +223,12 @@ class TemplateField(models.Model): length = models.PositiveIntegerField( blank=True, null=True, verbose_name="Размер поля ввода" ) + default = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name="Значение по умолчанию", + ) class Meta: verbose_name = "Поле шаблона" @@ -215,7 +287,6 @@ def create_document_fields(self, fields_data): template_field = field_data["field"] template = TemplateField.objects.get(id=template_field.id).template if self.template == template: - # Эту проверку надо в валидатор засунуть. # Проверяется, принадлежит ли поле шаблону документа document_fields.append( DocumentField( @@ -237,7 +308,6 @@ class DocumentField(models.Model): related_name="document_fields", ) value = models.CharField(max_length=255, verbose_name="Содержимое поля") - # description = models.CharField(verbose_name="Описание поля", blank=True) document = models.ForeignKey( Document, on_delete=models.CASCADE, @@ -254,27 +324,6 @@ def __str__(self): """Отображение - шаблон поле.""" return f"{self.field.template} {self.field}" - # class FieldToDocument(models.Model): - # """Связь полей и документов.""" - - # document = models.ForeignKey( - # Document, - # on_delete=models.CASCADE, - # related_name="document_of_field", - # ) - # fields = models.ForeignKey( - # DocumentField, - # on_delete=models.CASCADE, - # related_name="fields_of_document", - # ) - - # class Meta: - # verbose_name = "Связь между полем и документом" - # verbose_name_plural = "Связи между полями и документами" - - # def __str__(self): - # return f"{self.document} {self.fields}" - class FavTemplate(models.Model): """Избранные шаблоны.""" diff --git a/backend/documents/views.py b/backend/documents/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/backend/documents/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/backend/request/documents.http b/backend/request/documents.http index 32ec0d5..4103db4 100644 --- a/backend/request/documents.http +++ b/backend/request/documents.http @@ -1,22 +1,22 @@ -@TOKEN = Token bb4f5dbedaafd2730f35803e7087e797479d68f5 -@URL = https://documents-template.site/ +@TOKEN = Token c911144116cd1689f8fd01e00102bcd1ce4cc175 +# @URL = https://documents-template.site/ +@URL = http://127.0.0.1:8000/api/v2 ### -GET {{URL}}/api/documents/32/ -Content-Type: application/json +GET {{URL}}/documents/1 Authorization: {{TOKEN}} { } ### -POST {{URL}}/api/documents/ +POST {{URL}}/documents/ Content-Type: application/json Authorization: {{TOKEN}} { "description": "doc1", - "template": 1, + "template": 42, "completed": true, "document_fields": [ { @@ -63,7 +63,7 @@ Authorization: {{TOKEN}} } ### -PATCH {{URL}}/api/documents/2/ +PATCH {{URL}}/documents/2/ Content-Type: application/json Authorization: {{TOKEN}} @@ -115,7 +115,7 @@ Authorization: {{TOKEN}} } ### -get {{URL}}/api/documents/draft/ +get {{URL}}/documents/draft/ Content-Type: application/json Authorization: {{TOKEN}} @@ -123,7 +123,7 @@ Authorization: {{TOKEN}} } ### -get {{URL}}/api/documents/history/ +get {{URL}}/documents/history/ Content-Type: application/json Authorization: {{TOKEN}} @@ -131,7 +131,7 @@ Authorization: {{TOKEN}} } ### -get {{URL}}/api/documents/2/download_document/ +get {{URL}}/documents/2/download_document/ Content-Type: application/json Authorization: {{TOKEN}} diff --git a/backend/request/objects.http b/backend/request/objects.http new file mode 100644 index 0000000..b52d468 --- /dev/null +++ b/backend/request/objects.http @@ -0,0 +1,37 @@ +@TOKEN = Token 66ba19730c8aaad7378dc20fae75656b06b00e02 + +# @URL = https://documents-template.site/ +@URL = http://localhost:8000/api/v2 + + +### Все базовые обьекты +GET {{URL}}/base_objects/ +Content-Type: application/json +# Authorization: {{TOKEN}} + +{ +} + +### Все поля базового обьекта +GET {{URL}}/base_object_fields/ +Content-Type: application/json +# Authorization: {{TOKEN}} + +{ +} + +### Все поля базового обьекта +GET {{URL}}/objects/ +Content-Type: application/json +# Authorization: {{TOKEN}} + +{ +} + +### Все поля базового обьекта +GET {{URL}}/object_fields/ +Content-Type: application/json +# Authorization: {{TOKEN}} + +{ +} \ No newline at end of file diff --git a/backend/request/template.http b/backend/request/template.http index 46c00cf..f83c612 100644 --- a/backend/request/template.http +++ b/backend/request/template.http @@ -1,18 +1,19 @@ -@TOKEN = Token 640063c217c9693de37f04d65249f35a19705efb -# @URL = https://documents-template.site/ -@URL = http://localhost:8000 +@TOKEN = Token 66ba19730c8aaad7378dc20fae75656b06b00e02 + +@URL = https://doky.pro/api/v2 +# @URL = http://localhost:8000/api/v2 ### Все шаблоны -GET {{URL}}/api/templates/ +GET {{URL}}/templates/ Content-Type: application/json -Authorization: {{TOKEN}} +# Authorization: {{TOKEN}} { } ### Просмотр одного шаблона -GET {{URL}}/api/templates/1/ +GET {{URL}}/templates/22/ Content-Type: application/json Authorization: {{TOKEN}} @@ -21,7 +22,7 @@ Authorization: {{TOKEN}} ### Просмотр всех полей шаблона -GET {{URL}}/api/templates/1/fields +GET {{URL}}/templates/42/fields Content-Type: application/json Authorization: {{TOKEN}} @@ -29,15 +30,7 @@ Authorization: {{TOKEN}} } ### Добавить шаблон в избранное -POST {{URL}}/api/templates/1/favorite/ -Content-Type: application/json -Authorization: {{TOKEN}} - -{ -} - -### Удалить шаблон из избранного -DELETE {{URL}}/api/templates/1/favorite/ +POST {{URL}}/templates/22/favorite/ Content-Type: application/json Authorization: {{TOKEN}} @@ -45,7 +38,7 @@ Authorization: {{TOKEN}} } ### Удалить шаблон из избранного -DELETE {{URL}}/api/templates/1/favorite/ +DELETE {{URL}}/templates/1/favorite/ Content-Type: application/json Authorization: {{TOKEN}} @@ -54,7 +47,7 @@ Authorization: {{TOKEN}} ### Получить первью документа от анонимного пользователя -POST {{URL}}/api/templates/1/download_preview/ +POST {{URL}}/templates/22/download_preview/ Content-Type: application/json # Authorization: {{TOKEN}} @@ -97,4 +90,106 @@ Content-Type: application/json "field": 9 } ] -} \ No newline at end of file +} + +### Добавить новый шаблон и описание его полей в базу (ТОЛЬКО ДЛЯ АДМИНА) +POST {{URL}}/templates/ +Content-Type: application/json +Authorization: {{TOKEN}} + +{ + "name": "Заявление в детский сад", + "deleted": true, + "description": "Данный шаблон необходим для заполнения заявления в детский сад. Заявление может быть составлено в простой письменной форме. Можно доработать шаблон под себя и прописать наиболее важные поля после его скачивания.", + "fields": [ + { + "tag": "ДетскийСадНомерНазвание", + "name": "Номер и название детского сада", + "hint": "66 Непоседы", + "group": 1, + "type": "str20", + "length": 40 + }, + { + "tag": "АдресатФИО", + "name": "ФИО заведующего (укажите в дательном падеже)", + "hint": "Ивановой Ирине Петровне", + "group": 2, + "type": "fio", + "length": 40 + + }, + { + "tag": "ОтправительФИО", + "name": "ФИО родителя/законного представителя (в родительном падеже)", + "hint": "Иванова Ивана Ивановича", + "group": 3, + "type": "fio", + "length": 40 + }, + { + "tag": "ОтправительПочтовыйАдрес", + "name": "Почтовый адрес", + "hint": "город, улица, номер квартиры", + "group": 3, + "type": "str40", + "length": 40 + }, + { + "tag": "РебенокФИО", + "name": "ФИО ребенка (в творительном падеже)", + "hint": "Ивановым Данилой Ивановичем", + "group": 4, + "type": "fio", + "length": 40 + }, + { + "tag": "РебенокГруппа", + "name": "Номер или название группы", + "hint": "№3 или средняя", + "group": 4, + "type": "str20", + "length": 40 + }, + { + "tag": "Дата1", + "name": "Дата начала отпуска", + "hint": "дд.мм.гггг", + "type": "date", + "length": 40 + }, + { + "tag": "Дата2", + "name": "Дата окончания отпуска", + "hint": "дд.мм.гггг", + "type": "date", + "length": 40 + }, + { + "tag": "Дата3", + "name": "Дата подачи заявления", + "hint": "дд.мм.гггг", + "type": "date", + "length": 40 + } + ], + "groups": [ + { + "id": 1, + "name": "Данные о детском саде" + }, + { + "id": 2, + "name": "Адресат" + }, + { + "id": 3, + "name": "Отправитель" + }, + { + "id": 4, + "name": "Ребенок" + } + ] +} + diff --git a/backend/request/users.http b/backend/request/users.http index d2f6eed..29b9889 100644 --- a/backend/request/users.http +++ b/backend/request/users.http @@ -1,13 +1,13 @@ -@TOKEN = Token b5b767cb36da725f900892f627a28dc1c0e40488 -@URL = https://documents-template.site -# @URL = http://127.0.0.1:8000 +@TOKEN = Token c911144116cd1689f8fd01e00102bcd1ce4cc175 +@URL = https://doki.pro/api/v2 +# @URL = http://127.0.0.1:8000/api/v2 ### -POST {{URL}}/api/users/ +POST {{URL}}/users/ Content-Type: application/json { - "email": "nikox11882@mail.ru", + "email": "nikox1181182@mail.ru", "username": "nikox1122@mail.ru", "password": "456852Zx", "first_name": "kewk", @@ -15,16 +15,16 @@ Content-Type: application/json } ### -POST {{URL}}/api/auth/token/login/ +POST {{URL}}/auth/token/login/ Content-Type: application/json { - "email": "nikox112@mail.ru", + "email": "nikox1181182@mail.ru", "password": "456852Zx" } ### -POST {{URL}}/api/users/set_password/ +POST {{URL}}/users/set_password/ Content-Type: application/json Authorization: {{TOKEN}} @@ -34,7 +34,7 @@ Authorization: {{TOKEN}} } ### -GET {{URL}}/api/users/me/ +GET {{URL}}/users/me/ Content-Type: application/json Authorization: {{TOKEN}} @@ -43,7 +43,7 @@ Authorization: {{TOKEN}} ### -GET {{URL}}/api/users/2/ +GET {{URL}}/users/2/ Content-Type: application/json Authorization: {{TOKEN}} diff --git a/backend/requirements.txt b/backend/requirements.txt index 43f53b5..88cfc97 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,20 +1,22 @@ asgiref==3.7.2 -aspose-words==23.10.0 atomicwrites==1.4.1 attrs==23.1.0 Babel==2.13.0 black==23.9.1 certifi==2023.7.22 cffi==1.16.0 +cfgv==3.4.0 charset-normalizer==2.0.12 click==8.1.7 colorama==0.4.6 coreapi==2.3.3 coreschema==0.0.4 -cryptography==41.0.4 +# cryptography==41.0.4 DAWG-Python==0.7.2 defusedxml==0.8.0rc2 +distlib==0.3.7 Django==3.2 +django-allauth==0.61.1 django-colorfield==0.9.0 django-cors-headers==4.1.0 django-filter==22.1 @@ -26,8 +28,11 @@ docopt==0.6.2 docxcompose==1.4.0 docxtpl==0.16.7 drf-yasg==1.21.7 +filelock==3.12.4 gunicorn==20.1.0 +identify==2.5.30 idna==3.4 +inflection==0.5.1 iniconfig==2.0.0 isort==5.12.0 itypes==1.2.0 @@ -35,14 +40,15 @@ Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 mypy-extensions==1.0.0 +nodeenv==1.8.0 num2words==0.5.12 oauthlib==3.2.2 packaging==23.2 pathspec==0.11.2 -Pillow==9.0.0 +# Pillow==9.0.0 platformdirs==3.11.0 pluggy==0.13.1 -psycopg2-binary==2.9.3 +psycopg2-binary==2.9.9 py==1.11.0 pycparser==2.21 PyJWT==2.1.0 @@ -55,8 +61,10 @@ python-docx==0.8.11 python-dotenv==0.19.0 python3-openid==3.2.0 pytz==2023.3.post1 +PyYAML==6.0.1 requests==2.26.0 requests-oauthlib==1.3.1 +sentry-sdk==1.38.0 six==1.16.0 social-auth-app-django==4.0.0 social-auth-core==4.4.2 @@ -66,4 +74,5 @@ tomli==2.0.1 typing_extensions==4.8.0 uritemplate==4.1.1 urllib3==1.26.17 +virtualenv==20.24.5 webcolors==1.11.1 diff --git a/backend/users/migrations/0002_alter_user_email.py b/backend/users/migrations/0002_alter_user_email.py new file mode 100644 index 0000000..0946ee8 --- /dev/null +++ b/backend/users/migrations/0002_alter_user_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2023-11-24 13:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(error_messages={'unique': 'Данная почта уже используется'}, max_length=254, unique=True, verbose_name='email address'), + ), + ] diff --git a/backend/users/migrations/0003_alter_user_password.py b/backend/users/migrations/0003_alter_user_password.py new file mode 100644 index 0000000..158a947 --- /dev/null +++ b/backend/users/migrations/0003_alter_user_password.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2024-04-04 08:47 + +from django.db import migrations, models +import users.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_alter_user_email'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(max_length=150, validators=[users.validators.validator_password]), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 1bf4a3a..096eb8a 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,11 +1,24 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from .validators import validator_password + class User(AbstractUser): bio = models.TextField("Биография", null=True, blank=True) email = models.EmailField( - verbose_name="email address", max_length=254, unique=True + verbose_name="email address", + max_length=254, + unique=True, + error_messages={ + "unique": "Данная почта уже используется", + }, + ) + password = models.CharField( + max_length=150, + validators=[ + validator_password, + ], ) class Meta: diff --git a/backend/users/tests.py b/backend/users/tests.py index 19b0c86..d5c6a87 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -24,7 +24,7 @@ def setUpClass(cls): "password": "qwertyqwerty123", } cls.response_registration = cls.client.post( - "/api/users/", cls.test_user_data + "/api/v2/users/", cls.test_user_data ) @classmethod @@ -45,10 +45,10 @@ def test_registration(self): def test_authentication(self): response_registration = self.response_registration response_token_login = self.client.post( - "/api/auth/token/login/", self.test_user_data + "/api/v2/auth/token/login/", self.test_user_data ) response_auth = self.client.get( - "/api/users/me/", + "/api/v2/users/me/", HTTP_AUTHORIZATION=f'Token {response_token_login.data["auth_token"]}', ) self.assertEqual( diff --git a/backend/users/validators.py b/backend/users/validators.py index 6c17282..c42e326 100644 --- a/backend/users/validators.py +++ b/backend/users/validators.py @@ -1,7 +1,27 @@ +import re from django.core.exceptions import ValidationError +# Латинские буквы +# Одна заглавная + +# Одна строчная +# Одна цифра +# Один из спецсимволов (!#$%*&@^) +# Длина поля: +# Min 8 символов +# Max 32 символов -def validator_username(value): - if value.lower() == "me": - raise ValidationError("me слово запрещенное, в любом регистре. ") +def validator_password(value): + + if value.len() > 8 and value < 32: + raise ValidationError("Пароль должен сожержать больше 8 или меньше 32 символов") + + pattern = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$' + if re.match(pattern, value) is None: + raise ValidationError('Password has incorrecr format.') + + pattern = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$' + if re.match(pattern, value) is None: + raise ValidationError('Password has incorrecr format.') + return value + diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 07b52ee..eb6f5ed 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -1,3 +1,4 @@ + version: '3.8' volumes: @@ -15,7 +16,6 @@ services: backend: image: documents23/document-template-engine_backend:latest - command: ["commands/app.sh"] env_file: .env volumes: - static:/app/static/ @@ -26,19 +26,15 @@ services: frontend: image: documents23/document-template-engine_frontend:latest env_file: .env + command: cp -r /app/build/. /static/ volumes: - - ./frontend/:/app/result_build/ + - static:/static - nginx: - image: nginx:1.19.3 + gateway: + image: documents23/document-template-engine_gateway:latest + env_file: .env ports: - - "8000:80" + - 9000:80 volumes: - - ./proxy-server/nginx.conf:/etc/nginx/conf.d/default.conf - - ./frontend/build:/usr/share/nginx/html/ - - ./docs/:/usr/share/nginx/html/api/docs/ - - static:/var/html/static/ - - media:/var/html/media/ - depends_on: - - backend - restart: always \ No newline at end of file + - static:/staticfiles/ + - media:/app/media/ diff --git a/docker-compose.yml b/docker-compose.yml index ce0efa4..e36e88f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: backend: build: ./backend/ - command: ["commands/app.sh"] env_file: .env volumes: - static:/app/static/ @@ -24,20 +23,18 @@ services: - db frontend: + platform: linux/x86_64 image: documents23/document-template-engine_frontend:latest + env_file: .env + command: cp -r /app/build/. /static/ volumes: - - ./frontend/:/app/result_build/ + - static:/static - nginx: - image: nginx:1.19.3 + gateway: + build: ./gateway/ + env_file: .env ports: - - "8000:80" + - 9000:80 volumes: - - ./proxy-server/nginx.conf:/etc/nginx/conf.d/default.conf - - ./frontend/build:/usr/share/nginx/html/ - - ./docs/:/usr/share/nginx/html/api/docs/ - - static:/var/html/static/ - - media:/var/html/media/ - depends_on: - - backend - restart: always + - static:/staticfiles/ + - media:/app/media/ \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..f0808a1 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:1.22.1 +COPY nginx.conf /etc/nginx/templates/default.conf.template diff --git a/gateway/nginx.conf b/gateway/nginx.conf new file mode 100644 index 0000000..0832f32 --- /dev/null +++ b/gateway/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 80; + index index.html; + + location /api/ { + proxy_set_header Host $http_host; + proxy_pass http://backend:9000/api/; + } + location /accounts/ { + proxy_set_header Host $http_host; + proxy_pass http://backend:9000/accounts/; + } + location /admin/ { + proxy_set_header Host $http_host; + proxy_pass http://backend:9000/admin/; + } + location /media/ { + proxy_set_header Host $http_host; + alias /app/media/; + } + + location /static/admin/ { + proxy_set_header Host $http_host; + alias /staticfiles/admin/; + } + + location /static/rest_framework/ { + proxy_set_header Host $http_host; + alias /staticfiles/rest_framework/; + } + + location /static/drf-yasg/ { + proxy_set_header Host $http_host; + alias /staticfiles/drf-yasg/; + } + + location / { + proxy_set_header Host $http_host; + alias /staticfiles/; + try_files $uri $uri/ /index.html; + } +} diff --git a/proxy-server/nginx.conf b/proxy-server/nginx.conf deleted file mode 100644 index 8b078fc..0000000 --- a/proxy-server/nginx.conf +++ /dev/null @@ -1,52 +0,0 @@ -server { - listen 80; - - location /media/ { - proxy_set_header Host $http_host; - root /var/html/; - } - - location /static/admin/ { - proxy_set_header Host $http_host; - root /var/html/; - } - - location /static/rest_framework/ { - proxy_set_header Host $http_host; - root /var/html/; - } - - location /static/drf-yasg/ { - proxy_set_header Host $http_host; - root /var/html/; - } - - location /admin/ { - proxy_set_header Host $http_host; - proxy_pass http://backend:9000/admin/; - } - - location /api/ { - proxy_set_header Host $http_host; - proxy_pass http://backend:9000/api/; - } - - location /swagger/ { - proxy_set_header X-Forwarded-Protocol $scheme; - proxy_set_header Host $http_host; - proxy_pass http://backend:9000/swagger/; - } - - location /redoc/ { - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-Protocol $scheme; - proxy_pass http://backend:9000/redoc/; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - - } -} -