From d85988277ff85b6926cc4cef1a9e1c25f5d80a25 Mon Sep 17 00:00:00 2001 From: Alexander Timchenko Date: Wed, 8 Nov 2023 12:12:37 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8?= =?UTF-8?q?=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=B5=D0=B9=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/v1/serializers.py | 107 +++++++++++++----- backend/api/v1/views.py | 28 ++--- backend/documents/admin.py | 10 +- .../migrations/0027_auto_20231108_1053.py | 27 +++++ .../0028_remove_documentfield_description.py | 17 +++ backend/documents/models.py | 94 +++++++++------ 6 files changed, 199 insertions(+), 84 deletions(-) create mode 100644 backend/documents/migrations/0027_auto_20231108_1053.py create mode 100644 backend/documents/migrations/0028_remove_documentfield_description.py diff --git a/backend/api/v1/serializers.py b/backend/api/v1/serializers.py index a9c3da3..7d745e2 100644 --- a/backend/api/v1/serializers.py +++ b/backend/api/v1/serializers.py @@ -12,13 +12,13 @@ DocumentField, FavDocument, FavTemplate, - FieldToDocument, Template, TemplateField, ) User = get_user_model() + class Base64ImageField(serializers.ImageField): def to_internal_value(self, data): if isinstance(data, str) and data.startswith("data:image"): @@ -29,6 +29,7 @@ def to_internal_value(self, data): return super().to_internal_value(data) + class TemplateFieldSerializer(serializers.ModelSerializer): """Сериализатор поля шаблона.""" @@ -104,6 +105,7 @@ class TemplateSerializerMinified(serializers.ModelSerializer): is_favorited = serializers.SerializerMethodField() image = Base64ImageField(required=True, allow_null=True) + class Meta: model = Template exclude = ("template",) @@ -181,13 +183,12 @@ class DocumentFieldSerializer(serializers.ModelSerializer): class Meta: model = DocumentField - fields = "__all__" + exclude = ("document",) -class DocumentReadSerializer(serializers.ModelSerializer): - """Сериализатор документов.""" +class DocumentReadSerializerMinified(serializers.ModelSerializer): + """Сериализатор документов сокращенный (без информации о полях)""" - document_fields = DocumentFieldSerializer(many=True) is_favorited = serializers.SerializerMethodField() class Meta: @@ -200,7 +201,6 @@ class Meta: "description", "template", "owner", - "document_fields", "is_favorited", ) @@ -213,6 +213,67 @@ def get_is_favorited(self, document: Document) -> bool: ).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): """Сериализатор документов.""" @@ -231,41 +292,27 @@ class Meta: @transaction.atomic def create(self, validated_data): - """Пока говно код. Создание документа и полей документа""" + """Создание документа и полей документа""" document_fields = validated_data.pop("document_fields") document = Document.objects.create(**validated_data) - for data in document_fields: - field = data["field"] - template = TemplateField.objects.get(id=field.id).template - if ( - document.template == template - ): # Эту проверку надо в валидатор засунуть. - # Проверяется, принадлежит ли поле выбраному шаблону - field = DocumentField.objects.create( - field=field, value=data["value"] - ) - FieldToDocument.objects.create(fields=field, document=document) + 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 = Document.objects.get(id=instance.id) - FieldToDocument.objects.filter(document=document).delete() - for data in document_fields: - field = data["field"] - template = TemplateField.objects.get(id=field.id).template - if document.template == template: - # Эту проверку надо в валидатор засунуть. - # Проверяется, принадлежит ли поле выбраному шаблону - field = DocumentField.objects.create( - field=data["field"], value=data["value"] - ) - FieldToDocument.objects.create(fields=field, document=document) + document.document_fields.all().delete() + document.create_document_fields(document_fields) return instance + def to_representation(self, instance): + return DocumentReadSerializerMinified( + instance, context={"request": self.context.get("request")} + ).data + class FavTemplateSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/api/v1/views.py b/backend/api/v1/views.py index 4d88fb4..6acf1b0 100644 --- a/backend/api/v1/views.py +++ b/backend/api/v1/views.py @@ -24,10 +24,10 @@ from api.v1.permissions import IsOwner, IsOwnerOrAdminOrReadOnly from api.v1.serializers import ( CategorySerializer, - DocumentFieldForPreviewSerializer, DocumentFieldSerializer, - DocumentReadSerializer, + DocumentReadSerializerMinified, + DocumentReadSerializerExtended, DocumentWriteSerializer, FavDocumentSerializer, FavTemplateSerializer, @@ -35,16 +35,15 @@ TemplateSerializer, TemplateSerializerMinified, ) + # from api.v1.utils import Util from core.constants import Messages from core.template_render import DocumentTemplate from documents.models import ( Category, Document, - DocumentField, FavDocument, FavTemplate, - FieldToDocument, Template, ) @@ -151,7 +150,7 @@ class DocumentViewSet(viewsets.ModelViewSet): """Документ.""" queryset = Document.objects.all() - serializer_class = DocumentReadSerializer + serializer_class = DocumentReadSerializerMinified http_method_names = ("get", "post", "patch", "delete") permissions_classes = (IsAuthenticated,) filter_backends = ( @@ -171,8 +170,10 @@ def get_queryset(self): def get_serializer_class(self): """Выбор сериализатора.""" - if self.action in ["list", "retrieve"]: - return DocumentReadSerializer + if self.action == "retrieve": + return DocumentReadSerializerExtended + elif self.action == "list": + return DocumentReadSerializerMinified return DocumentWriteSerializer def perform_create(self, serializer): @@ -189,7 +190,7 @@ def draft_documents(self, request): """Возвращает список незаконченных документов/черновиков""" user = self.request.user queryset = Document.objects.filter(completed=False, owner=user) - serializer = DocumentReadSerializer( + serializer = DocumentReadSerializerMinified( queryset, many=True, context={"request": request} ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -220,9 +221,9 @@ def download_document(self, request, pk=None): document = get_object_or_404(Document, id=pk) context = dict() - for docfield in FieldToDocument.objects.filter(document=document): - template_field = docfield.fields.field - context[template_field.tag] = docfield.fields.value + 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() } @@ -241,7 +242,7 @@ 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) + b"".join(self.download_document(request, pk).streaming_content) ) docx_file = aw.Document(docx_stream) buffer = io.BytesIO() @@ -267,8 +268,7 @@ def get_queryset(self): or document.owner != self.request.user ): raise PermissionDenied() - through_set = FieldToDocument.objects.filter(document=document).all() - return DocumentField.objects.filter(fieldtodocument__in=through_set) + return document.document_fields.objects.all() class FavTemplateAPIview(APIView): diff --git a/backend/documents/admin.py b/backend/documents/admin.py index e65d2b8..d671096 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -3,8 +3,6 @@ from documents import models -admin.site.register(models.FieldToDocument) - @admin.register(models.Category) class CategoryAdmin(admin.ModelAdmin): @@ -104,6 +102,11 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) +class DocumentFieldInlineAdmin(admin.TabularInline): + model = models.DocumentField + extra = 1 + + @admin.register(models.Document) class DocumentAdmin(admin.ModelAdmin): list_display = ( @@ -117,11 +120,12 @@ class DocumentAdmin(admin.ModelAdmin): ) list_filter = ("template", "owner", "completed") readonly_fields = ("id", "created", "updated") + inlines = (DocumentFieldInlineAdmin,) @admin.register(models.DocumentField) class DocumentFieldAdmin(admin.ModelAdmin): - list_display = ("id", "field_id", "value", "description") + list_display = ("id", "document_id", "field_id", "value") readonly_fields = ("id",) diff --git a/backend/documents/migrations/0027_auto_20231108_1053.py b/backend/documents/migrations/0027_auto_20231108_1053.py new file mode 100644 index 0000000..151d890 --- /dev/null +++ b/backend/documents/migrations/0027_auto_20231108_1053.py @@ -0,0 +1,27 @@ +# 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 new file mode 100644 index 0000000..5e0e3bc --- /dev/null +++ b/backend/documents/migrations/0028_remove_documentfield_description.py @@ -0,0 +1,17 @@ +# 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 de547bf..f90dffe 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -56,6 +56,7 @@ class Template(models.Model): null=True, blank=True, ) + class Meta: verbose_name = "Шаблон" verbose_name_plural = "Шаблоны" @@ -157,7 +158,6 @@ class TemplateField(models.Model): blank=True, null=True, verbose_name="Размер поля ввода" ) - class Meta: verbose_name = "Поле шаблона" verbose_name_plural = "Поля шаблона" @@ -174,28 +174,6 @@ def clean(self): raise ValidationError(Messages.WRONG_FIELD_AND_GROUP_TEMPLATES) -class DocumentField(models.Model): - """Поля документа.""" - - field = models.ForeignKey( - TemplateField, - on_delete=models.PROTECT, - verbose_name="Поле", - related_name="document_fields", - ) - value = models.CharField(max_length=255, verbose_name="Содержимое поля") - description = models.TextField(verbose_name="Описание поля", blank=True) - - class Meta: - verbose_name = "Поле документа" - verbose_name_plural = "Поля документа" - ordering = ("field__template", "field") - - def __str__(self): - """Отображение - шаблон поле.""" - return f"{self.field.template} {self.field}" - - class Document(models.Model): """Документ.""" @@ -219,9 +197,6 @@ class Document(models.Model): verbose_name="Документ заполнен", default=False ) description = models.TextField(verbose_name="Описание документа") - document_fields = models.ManyToManyField( - DocumentField, through="FieldToDocument" - ) class Meta: verbose_name = "Документ" @@ -233,27 +208,72 @@ def __str__(self): """Автор документа и название шаблона.""" return f"{self.owner} {self.template}" + def create_document_fields(self, fields_data): + """Создание полей для данного документа по данным из fields_data""" + document_fields = [] + for field_data in fields_data: + template_field = field_data["field"] + template = TemplateField.objects.get(id=template_field.id).template + if self.template == template: + # Эту проверку надо в валидатор засунуть. + # Проверяется, принадлежит ли поле шаблону документа + document_fields.append( + DocumentField( + field=template_field, + value=field_data["value"], + document=self, + ) + ) + DocumentField.objects.bulk_create(document_fields) + -class FieldToDocument(models.Model): - """Связь полей и документов.""" +class DocumentField(models.Model): + """Поля документа.""" + field = models.ForeignKey( + TemplateField, + on_delete=models.PROTECT, + verbose_name="Поле", + 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, - related_name="document_of_field", - ) - fields = models.ForeignKey( - DocumentField, - on_delete=models.CASCADE, - related_name="fields_of_document", + verbose_name="Документ", + related_name="document_fields", ) class Meta: - verbose_name = "Связь между полем и документом" - verbose_name_plural = "Связи между полями и документами" + verbose_name = "Поле документа" + verbose_name_plural = "Поля документа" + ordering = ("field__template", "field") def __str__(self): - return f"{self.document} {self.fields}" + """Отображение - шаблон поле.""" + 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): From a1128593d1fcf19121d2e58db0bdf741bb3068fa Mon Sep 17 00:00:00 2001 From: Alexander Timchenko Date: Wed, 8 Nov 2023 17:50:07 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20(=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BF=D0=BE=D0=BB=D1=8F=D0=BC=D0=B8=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/documents/admin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/documents/admin.py b/backend/documents/admin.py index d671096..8ca55db 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -106,6 +106,13 @@ class DocumentFieldInlineAdmin(admin.TabularInline): model = models.DocumentField extra = 1 + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "field" and request._document_instance_: + template = request._document_instance_.template + if template: + kwargs["queryset"] = template.fields.all() + return super().formfield_for_foreignkey(db_field, request, **kwargs) + @admin.register(models.Document) class DocumentAdmin(admin.ModelAdmin): @@ -122,12 +129,27 @@ class DocumentAdmin(admin.ModelAdmin): readonly_fields = ("id", "created", "updated") inlines = (DocumentFieldInlineAdmin,) + def get_form(self, request, instance=None, **kwargs): + request._document_instance_ = instance + return super().get_form(request, instance, **kwargs) + @admin.register(models.DocumentField) class DocumentFieldAdmin(admin.ModelAdmin): list_display = ("id", "document_id", "field_id", "value") readonly_fields = ("id",) + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "field": + docfield_id = request.resolver_match.kwargs.get("object_id") + if docfield_id: + docfield = models.DocumentField.objects.get(id=docfield_id) + if docfield.document.template: + kwargs[ + "queryset" + ] = docfield.document.template.fields.all() + return super().formfield_for_foreignkey(db_field, request, **kwargs) + admin.site.site_header = "Административная панель Шаблонизатор" admin.site.index_title = "Настройки Шаблонизатор"