From be76908788e829a55cb8b411b352440b43cf3ea6 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:35:53 +0300 Subject: [PATCH] F: Implement editors change for OSS -> RSForm --- .../migrations/0002_alter_editor_editor.py | 21 ++++++ .../0003_alter_librarytemplate_lib_source.py | 19 ++++++ .../backend/apps/library/models/Editor.py | 54 ++++++++++----- .../apps/library/models/LibraryItem.py | 5 +- .../apps/library/models/LibraryTemplate.py | 3 +- .../apps/library/serializers/data_access.py | 2 +- .../apps/library/tests/s_models/t_Editor.py | 65 ++++++++++++------- .../apps/library/tests/s_views/t_library.py | 8 +-- .../backend/apps/library/views/library.py | 28 +++++++- .../apps/oss/models/OperationSchema.py | 2 +- .../oss/tests/s_views/t_change_attributes.py | 20 +++++- .../backend/apps/oss/tests/s_views/t_oss.py | 2 +- rsconcept/backend/shared/EndpointTester.py | 4 +- 13 files changed, 177 insertions(+), 56 deletions(-) create mode 100644 rsconcept/backend/apps/library/migrations/0002_alter_editor_editor.py create mode 100644 rsconcept/backend/apps/library/migrations/0003_alter_librarytemplate_lib_source.py diff --git a/rsconcept/backend/apps/library/migrations/0002_alter_editor_editor.py b/rsconcept/backend/apps/library/migrations/0002_alter_editor_editor.py new file mode 100644 index 00000000..2a0f5ae5 --- /dev/null +++ b/rsconcept/backend/apps/library/migrations/0002_alter_editor_editor.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.7 on 2024-08-06 19:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='editor', + name='editor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Редактор'), + ), + ] diff --git a/rsconcept/backend/apps/library/migrations/0003_alter_librarytemplate_lib_source.py b/rsconcept/backend/apps/library/migrations/0003_alter_librarytemplate_lib_source.py new file mode 100644 index 00000000..e9793716 --- /dev/null +++ b/rsconcept/backend/apps/library/migrations/0003_alter_librarytemplate_lib_source.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-08-06 19:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0002_alter_editor_editor'), + ] + + operations = [ + migrations.AlterField( + model_name='librarytemplate', + name='lib_source', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Источник'), + ), + ] diff --git a/rsconcept/backend/apps/library/models/Editor.py b/rsconcept/backend/apps/library/models/Editor.py index 16656250..79f8246e 100644 --- a/rsconcept/backend/apps/library/models/Editor.py +++ b/rsconcept/backend/apps/library/models/Editor.py @@ -1,14 +1,11 @@ ''' Models: Editor. ''' -from typing import TYPE_CHECKING +from typing import Iterable from django.db import transaction from django.db.models import CASCADE, DateTimeField, ForeignKey, Model from apps.users.models import User -if TYPE_CHECKING: - from .LibraryItem import LibraryItem - class Editor(Model): ''' Editor list. ''' @@ -20,8 +17,7 @@ class Editor(Model): editor: ForeignKey = ForeignKey( verbose_name='Редактор', to=User, - on_delete=CASCADE, - null=True + on_delete=CASCADE ) time_create: DateTimeField = DateTimeField( verbose_name='Дата добавления', @@ -38,17 +34,17 @@ class Editor(Model): return f'{self.item}: {self.editor}' @staticmethod - def add(item: 'LibraryItem', user: User) -> bool: + def add(item: int, user: int) -> bool: ''' Add Editor for item. ''' - if Editor.objects.filter(item=item, editor=user).exists(): + if Editor.objects.filter(item_id=item, editor_id=user).exists(): return False - Editor.objects.create(item=item, editor=user) + Editor.objects.create(item_id=item, editor_id=user) return True @staticmethod - def remove(item: 'LibraryItem', user: User) -> bool: + def remove(item: int, user: int) -> bool: ''' Remove Editor. ''' - editor = Editor.objects.filter(item=item, editor=user) + editor = Editor.objects.filter(item_id=item, editor_id=user).only('pk') if not editor.exists(): return False editor.delete() @@ -56,16 +52,40 @@ class Editor(Model): @staticmethod @transaction.atomic - def set(item: 'LibraryItem', users: list[User]): + def set(item: int, users: Iterable[int]): ''' Set editors for item. ''' - processed: list[User] = [] - for editor_item in Editor.objects.filter(item=item): - if editor_item.editor not in users: + processed: set[int] = set() + for editor_item in Editor.objects.filter(item_id=item).only('pk', 'editor_id'): + editor_id = editor_item.editor_id + if editor_id not in users: editor_item.delete() else: - processed.append(editor_item.editor) + processed.add(editor_id) + + for user in users: + if user not in processed: + processed.add(user) + Editor.objects.create(item_id=item, editor_id=user) + + @staticmethod + @transaction.atomic + def set_and_return_diff(item: int, users: Iterable[int]) -> tuple[list[int], list[int]]: + ''' Set editors for item and return diff. ''' + processed: list[int] = [] + deleted: list[int] = [] + added: list[int] = [] + for editor_item in Editor.objects.filter(item_id=item).only('pk', 'editor_id'): + editor_id = editor_item.editor_id + if editor_id not in users: + deleted.append(editor_id) + editor_item.delete() + else: + processed.append(editor_id) for user in users: if user not in processed: processed.append(user) - Editor.objects.create(item=item, editor=user) + added.append(user) + Editor.objects.create(item_id=item, editor_id=user) + + return (added, deleted) diff --git a/rsconcept/backend/apps/library/models/LibraryItem.py b/rsconcept/backend/apps/library/models/LibraryItem.py index 798a0776..ba25a004 100644 --- a/rsconcept/backend/apps/library/models/LibraryItem.py +++ b/rsconcept/backend/apps/library/models/LibraryItem.py @@ -16,7 +16,6 @@ from django.db.models import ( from apps.users.models import User -from .Editor import Editor from .Subscription import Subscription from .Version import Version @@ -119,9 +118,9 @@ class LibraryItem(Model): ''' Get all subscribers for this item. ''' return [subscription.user for subscription in Subscription.objects.filter(item=self.pk).only('user')] - def editors(self) -> list[User]: + def editors(self) -> QuerySet[User]: ''' Get all Editors of this item. ''' - return [item.editor for item in Editor.objects.filter(item=self.pk).only('editor')] + return User.objects.filter(editor__item=self.pk) def versions(self) -> QuerySet[Version]: ''' Get all Versions of this item. ''' diff --git a/rsconcept/backend/apps/library/models/LibraryTemplate.py b/rsconcept/backend/apps/library/models/LibraryTemplate.py index d454f1b3..ca0f2307 100644 --- a/rsconcept/backend/apps/library/models/LibraryTemplate.py +++ b/rsconcept/backend/apps/library/models/LibraryTemplate.py @@ -7,8 +7,7 @@ class LibraryTemplate(Model): lib_source: ForeignKey = ForeignKey( verbose_name='Источник', to='library.LibraryItem', - on_delete=CASCADE, - null=True + on_delete=CASCADE ) class Meta: diff --git a/rsconcept/backend/apps/library/serializers/data_access.py b/rsconcept/backend/apps/library/serializers/data_access.py index 4742ff2e..6dd79a2b 100644 --- a/rsconcept/backend/apps/library/serializers/data_access.py +++ b/rsconcept/backend/apps/library/serializers/data_access.py @@ -86,7 +86,7 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer): return [item.pk for item in instance.subscribers()] def get_editors(self, instance: LibraryItem) -> list[int]: - return [item.pk for item in instance.editors()] + return list(instance.editors().values_list('pk', flat=True)) def get_versions(self, instance: LibraryItem) -> list: return [VersionInnerSerializer(item).data for item in instance.versions()] diff --git a/rsconcept/backend/apps/library/tests/s_models/t_Editor.py b/rsconcept/backend/apps/library/tests/s_models/t_Editor.py index b02b75f5..779ed9f2 100644 --- a/rsconcept/backend/apps/library/tests/s_models/t_Editor.py +++ b/rsconcept/backend/apps/library/tests/s_models/t_Editor.py @@ -34,44 +34,65 @@ class TestEditor(TestCase): def test_add_editor(self): - self.assertTrue(Editor.add(self.item, self.user1)) - self.assertEqual(len(self.item.editors()), 1) - self.assertTrue(self.user1 in self.item.editors()) + self.assertTrue(Editor.add(self.item.pk, self.user1.pk)) + self.assertEqual(self.item.editors().count(), 1) + self.assertTrue(self.user1 in list(self.item.editors())) - self.assertFalse(Editor.add(self.item, self.user1)) - self.assertEqual(len(self.item.editors()), 1) + self.assertFalse(Editor.add(self.item.pk, self.user1.pk)) + self.assertEqual(self.item.editors().count(), 1) - self.assertTrue(Editor.add(self.item, self.user2)) - self.assertEqual(len(self.item.editors()), 2) + self.assertTrue(Editor.add(self.item.pk, self.user2.pk)) + self.assertEqual(self.item.editors().count(), 2) self.assertTrue(self.user1 in self.item.editors()) self.assertTrue(self.user2 in self.item.editors()) self.user1.delete() - self.assertEqual(len(self.item.editors()), 1) + self.assertEqual(self.item.editors().count(), 1) def test_remove_editor(self): - self.assertFalse(Editor.remove(self.item, self.user1)) - Editor.add(self.item, self.user1) - Editor.add(self.item, self.user2) - self.assertEqual(len(self.item.editors()), 2) + self.assertFalse(Editor.remove(self.item.pk, self.user1.pk)) + Editor.add(self.item.pk, self.user1.pk) + Editor.add(self.item.pk, self.user2.pk) + self.assertEqual(self.item.editors().count(), 2) - self.assertTrue(Editor.remove(self.item, self.user1)) - self.assertEqual(len(self.item.editors()), 1) + self.assertTrue(Editor.remove(self.item.pk, self.user1.pk)) + self.assertEqual(self.item.editors().count(), 1) self.assertTrue(self.user2 in self.item.editors()) - self.assertFalse(Editor.remove(self.item, self.user1)) + self.assertFalse(Editor.remove(self.item.pk, self.user1.pk)) def test_set_editors(self): - Editor.set(self.item, [self.user1]) - self.assertEqual(self.item.editors(), [self.user1]) + Editor.set(self.item.pk, [self.user1.pk]) + self.assertEqual(list(self.item.editors()), [self.user1]) - Editor.set(self.item, [self.user1, self.user1]) - self.assertEqual(self.item.editors(), [self.user1]) + Editor.set(self.item.pk, [self.user1.pk, self.user1.pk]) + self.assertEqual(list(self.item.editors()), [self.user1]) - Editor.set(self.item, []) - self.assertEqual(self.item.editors(), []) + Editor.set(self.item.pk, []) + self.assertEqual(list(self.item.editors()), []) - Editor.set(self.item, [self.user1, self.user2]) + Editor.set(self.item.pk, [self.user1.pk, self.user2.pk]) + self.assertEqual(set(self.item.editors()), set([self.user1, self.user2])) + + def test_set_editors_return_diff(self): + added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk]) + self.assertEqual(added, [self.user1.pk]) + self.assertEqual(deleted, []) + self.assertEqual(list(self.item.editors()), [self.user1]) + + added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk, self.user1.pk]) + self.assertEqual(added, []) + self.assertEqual(deleted, []) + self.assertEqual(list(self.item.editors()), [self.user1]) + + added, deleted = Editor.set_and_return_diff(self.item.pk, []) + self.assertEqual(added, []) + self.assertEqual(deleted, [self.user1.pk]) + self.assertEqual(list(self.item.editors()), []) + + added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk, self.user2.pk]) + self.assertEqual(added, [self.user1.pk, self.user2.pk]) + self.assertEqual(deleted, []) self.assertEqual(set(self.item.editors()), set([self.user1, self.user2])) diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index 26d4a584..1d02c78f 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -197,18 +197,18 @@ class TestLibraryViewset(EndpointTester): self.executeOK(data=data, item=self.owned.pk) self.owned.refresh_from_db() self.assertEqual(self.owned.time_update, time_update) - self.assertEqual(self.owned.editors(), [self.user]) + self.assertEqual(list(self.owned.editors()), [self.user]) self.executeOK(data=data) - self.assertEqual(self.owned.editors(), [self.user]) + self.assertEqual(list(self.owned.editors()), [self.user]) data = {'users': [self.user2.pk]} self.executeOK(data=data) - self.assertEqual(self.owned.editors(), [self.user2]) + self.assertEqual(list(self.owned.editors()), [self.user2]) data = {'users': []} self.executeOK(data=data) - self.assertEqual(self.owned.editors(), []) + self.assertEqual(list(self.owned.editors()), []) data = {'users': [self.user2.pk, self.user.pk]} self.executeOK(data=data) diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 0f9eb6dc..649e3c36 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -261,8 +261,32 @@ class LibraryViewSet(viewsets.ModelViewSet): item = self._get_item() serializer = s.UsersListSerializer(data=request.data) serializer.is_valid(raise_exception=True) - editors = serializer.validated_data['users'] - m.Editor.set(item=item, users=editors) + editors: list[int] = request.data['users'] + + with transaction.atomic(): + added, deleted = m.Editor.set_and_return_diff(item.pk, editors) + if len(added) >= 0 or len(deleted) >= 0: + owned_schemas = OperationSchema(item).owned_schemas().only('pk') + if owned_schemas.exists(): + m.Editor.objects.filter( + item__in=owned_schemas, + editor_id__in=deleted + ).delete() + + existing_editors = m.Editor.objects.filter( + item__in=owned_schemas, + editor__in=added + ).values_list('item_id', 'editor_id') + existing_editor_set = set(existing_editors) + + new_editors = [ + m.Editor(item=schema, editor_id=user) + for schema in owned_schemas + for user in added + if (item.id, user) not in existing_editor_set + ] + m.Editor.objects.bulk_create(new_editors) + return Response(status=c.HTTP_200_OK) diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 58f31b0c..5d7c997b 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -169,7 +169,7 @@ class OperationSchema: access_policy=self.model.access_policy, location=self.model.location ) - Editor.set(schema.model, self.model.editors()) + Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True)) operation.result = schema.model operation.save() self.save() diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py b/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py index d8f663da..5007616e 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py @@ -2,7 +2,7 @@ from rest_framework import status -from apps.library.models import AccessPolicy, LocationHead +from apps.library.models import AccessPolicy, Editor, LocationHead from apps.oss.models import Operation, OperationSchema, OperationType from apps.rsform.models import RSForm from apps.users.models import User @@ -105,3 +105,21 @@ class TestChangeAttributes(EndpointTester): self.assertNotEqual(self.ks1.model.access_policy, data['access_policy']) self.assertNotEqual(self.ks2.model.access_policy, data['access_policy']) self.assertEqual(self.ks3.access_policy, data['access_policy']) + + @decl_endpoint('/api/library/{item}/set-editors', method='patch') + def test_set_editors(self): + Editor.set(self.owned.model.pk, [self.user2.pk]) + Editor.set(self.ks1.model.pk, [self.user2.pk, self.user.pk]) + Editor.set(self.ks3.pk, [self.user2.pk, self.user.pk]) + data = {'users': [self.user3.pk]} + + self.executeOK(data=data, item=self.owned_id) + + self.owned.refresh_from_db() + self.ks1.refresh_from_db() + self.ks2.refresh_from_db() + self.ks3.refresh_from_db() + self.assertEqual(list(self.owned.model.editors()), [self.user3]) + self.assertEqual(list(self.ks1.model.editors()), [self.user, self.user2]) + self.assertEqual(list(self.ks2.model.editors()), []) + self.assertEqual(set(self.ks3.editors()), set([self.user, self.user3])) diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 2d669579..2a4fce62 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -220,7 +220,7 @@ class TestOssViewset(EndpointTester): @decl_endpoint('/api/oss/{item}/create-operation', method='post') def test_create_operation_schema(self): self.populateData() - Editor.add(self.owned.model, self.user2) + Editor.add(self.owned.model.pk, self.user2.pk) data = { 'item_data': { 'alias': 'Test4', diff --git a/rsconcept/backend/shared/EndpointTester.py b/rsconcept/backend/shared/EndpointTester.py index cde837f5..98d8bfe1 100644 --- a/rsconcept/backend/shared/EndpointTester.py +++ b/rsconcept/backend/shared/EndpointTester.py @@ -67,9 +67,9 @@ class EndpointTester(APITestCase): def toggle_editor(self, item: LibraryItem, value: bool = True): if value: - Editor.add(item, self.user) + Editor.add(item.pk, self.user.pk) else: - Editor.remove(item, self.user) + Editor.remove(item.pk, self.user.pk) def login(self): self.client.force_authenticate(user=self.user)