diff --git a/.vscode/settings.json b/.vscode/settings.json index a07cff1c..bf66d842 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,12 @@ }, "typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib", "eslint.workingDirectories": ["rsconcept/frontend"], + "[python]": { + "editor.formatOnType": true, + "editor.tabSize": 4, + "editor.insertSpaces": true + // "editor.defaultFormatter": "ms-python.autopep8" + }, "python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"], "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true, diff --git a/README.md b/README.md index 894be6ad..ea99477d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ This readme file is used mostly to document project dependencies
- Fira Code - Rubik - - Geologica + - Alegreya Sans SC - Noto Sans Math@@ -115,6 +115,7 @@ This readme file is used mostly to document project dependencies - Pylance - Pylint - Django + - autopep8 diff --git a/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py b/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py new file mode 100644 index 00000000..e21e509d --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-05-20 14:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0004_version'), + ] + + operations = [ + migrations.AlterModelOptions( + name='subscription', + options={'verbose_name': 'Подписка', 'verbose_name_plural': 'Подписки'}, + ), + migrations.AlterModelOptions( + name='version', + options={'verbose_name': 'Версия', 'verbose_name_plural': 'Версии'}, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0006_editor.py b/rsconcept/backend/apps/rsform/migrations/0006_editor.py new file mode 100644 index 00000000..75403aaa --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0006_editor.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2024-05-20 14:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0005_alter_subscription_options_alter_version_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Editor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')), + ('editor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Редактор')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Схема')), + ], + options={ + 'verbose_name': 'Редактор', + 'verbose_name_plural': 'Редакторы', + 'unique_together': {('item', 'editor')}, + }, + ), + ] diff --git a/rsconcept/backend/apps/rsform/models/Editor.py b/rsconcept/backend/apps/rsform/models/Editor.py new file mode 100644 index 00000000..bf0446a6 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Editor.py @@ -0,0 +1,58 @@ +''' Models: Editor. ''' +from typing import TYPE_CHECKING + +from django.db.models import ( + Model, ForeignKey, + CASCADE, DateTimeField +) + +from apps.users.models import User + + +if TYPE_CHECKING: + from .LibraryItem import LibraryItem + + +class Editor(Model): + ''' Editor list. ''' + item: ForeignKey = ForeignKey( + verbose_name='Схема', + to='rsform.LibraryItem', + on_delete=CASCADE + ) + editor: ForeignKey = ForeignKey( + verbose_name='Редактор', + to=User, + on_delete=CASCADE, + null=True + ) + time_create: DateTimeField = DateTimeField( + verbose_name='Дата добавления', + auto_now_add=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Редактор' + verbose_name_plural = 'Редакторы' + unique_together = [['item', 'editor']] + + def __str__(self) -> str: + return f'{self.item}: {self.editor}' + + @staticmethod + def add(user: User, item: 'LibraryItem') -> bool: + ''' Add Editor for item. ''' + if Editor.objects.filter(editor=user, item=item).exists(): + return False + Editor.objects.create(editor=user, item=item) + return True + + @staticmethod + def remove(user: User, item: 'LibraryItem') -> bool: + ''' Remove Editor. ''' + editor = Editor.objects.filter(editor=user, item=item) + if not editor.exists(): + return False + editor.delete() + return True diff --git a/rsconcept/backend/apps/rsform/models/LibraryItem.py b/rsconcept/backend/apps/rsform/models/LibraryItem.py index 06c6b317..13edd116 100644 --- a/rsconcept/backend/apps/rsform/models/LibraryItem.py +++ b/rsconcept/backend/apps/rsform/models/LibraryItem.py @@ -8,6 +8,7 @@ from django.db.models import ( from apps.users.models import User from .Version import Version from .Subscription import Subscription +from .Editor import Editor class LibraryItemType(TextChoices): @@ -64,7 +65,7 @@ class LibraryItem(Model): verbose_name_plural = 'Схемы' def __str__(self) -> str: - return f'{self.title}' + return f'{self.alias}' def get_absolute_url(self): return f'/api/library/{self.pk}' @@ -77,6 +78,10 @@ class LibraryItem(Model): ''' Get all Versions of this item. ''' return list(Version.objects.filter(item=self.pk).order_by('-time_create')) + def editors(self) -> list[Editor]: + ''' Get all Editors of this item. ''' + return [item.editor for item in Editor.objects.filter(item=self.pk).order_by('-time_create')] + @transaction.atomic def save(self, *args, **kwargs): subscribe = not self.pk and self.owner diff --git a/rsconcept/backend/apps/rsform/models/Subscription.py b/rsconcept/backend/apps/rsform/models/Subscription.py index 6d8d19ff..30d7ab91 100644 --- a/rsconcept/backend/apps/rsform/models/Subscription.py +++ b/rsconcept/backend/apps/rsform/models/Subscription.py @@ -25,8 +25,8 @@ class Subscription(Model): class Meta: ''' Model metadata. ''' - verbose_name = 'Подписки' - verbose_name_plural = 'Подписка' + verbose_name = 'Подписка' + verbose_name_plural = 'Подписки' unique_together = [['user', 'item']] def __str__(self) -> str: diff --git a/rsconcept/backend/apps/rsform/models/Version.py b/rsconcept/backend/apps/rsform/models/Version.py index 523602e0..2deff526 100644 --- a/rsconcept/backend/apps/rsform/models/Version.py +++ b/rsconcept/backend/apps/rsform/models/Version.py @@ -31,8 +31,8 @@ class Version(Model): class Meta: ''' Model metadata. ''' - verbose_name = 'Версии' - verbose_name_plural = 'Версия' + verbose_name = 'Версия' + verbose_name_plural = 'Версии' unique_together = [['item', 'version']] def __str__(self) -> str: diff --git a/rsconcept/backend/apps/rsform/models/__init__.py b/rsconcept/backend/apps/rsform/models/__init__.py index 3730aa18..a9aa0b54 100644 --- a/rsconcept/backend/apps/rsform/models/__init__.py +++ b/rsconcept/backend/apps/rsform/models/__init__.py @@ -5,4 +5,5 @@ from .Constituenta import Constituenta, CstType, _empty_forms from .LibraryItem import User, LibraryItem, LibraryItemType from .LibraryTemplate import LibraryTemplate from .Version import Version +from .Editor import Editor from .Subscription import Subscription diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 90eb9360..d1974c2a 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -52,6 +52,7 @@ class VersionCreateSerializer(serializers.ModelSerializer): class LibraryItemDetailsSerializer(serializers.ModelSerializer): ''' Serializer: LibraryItem detailed data. ''' subscribers = serializers.SerializerMethodField() + editors = serializers.SerializerMethodField() versions = serializers.SerializerMethodField() class Meta: @@ -63,6 +64,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer): def get_subscribers(self, instance: LibraryItem) -> list[int]: return [item.pk for item in instance.subscribers()] + def get_editors(self, instance: LibraryItem) -> list[int]: + return [item.pk for item in instance.editors()] + def get_versions(self, instance: LibraryItem) -> list: return [VersionInnerSerializer(item).data for item in instance.versions()] @@ -133,6 +137,9 @@ class RSFormSerializer(serializers.ModelSerializer): subscribers = serializers.ListField( child=serializers.IntegerField() ) + editors = serializers.ListField( + child=serializers.IntegerField() + ) items = serializers.ListField( child=CstSerializer() ) @@ -155,6 +162,7 @@ class RSFormSerializer(serializers.ModelSerializer): result = self.to_representation(cast(LibraryItem, self.instance)) del result['versions'] del result['subscribers'] + del result['editors'] del result['owner'] del result['is_common'] @@ -214,6 +222,9 @@ class RSFormParseSerializer(serializers.ModelSerializer): subscribers = serializers.ListField( child=serializers.IntegerField() ) + editors = serializers.ListField( + child=serializers.IntegerField() + ) items = serializers.ListField( child=CstDetailsSerializer() ) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/__init__.py b/rsconcept/backend/apps/rsform/tests/s_models/__init__.py index 4ba5390a..d3c0aefe 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/__init__.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/__init__.py @@ -1,4 +1,6 @@ ''' Tests for REST API. ''' from .t_Constituenta import * from .t_LibraryItem import * +from .t_Editor import * +from .t_Subscription import * from .t_RSForm import * diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py new file mode 100644 index 00000000..60db5e38 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py @@ -0,0 +1,62 @@ +''' Testing models: Editor. ''' +from django.test import TestCase + +from apps.rsform.models import LibraryItem, LibraryItemType, User, Editor + + +class TestEditor(TestCase): + ''' Testing Editor model. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + self.item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='КС1', + owner=self.user1 + ) + + + def test_default(self): + editors = list(Editor.objects.filter(item=self.item)) + self.assertEqual(len(editors), 0) + + + def test_str(self): + testStr = 'КС1: User2' + item = Editor.objects.create( + editor=self.user2, + item=self.item + ) + self.assertEqual(str(item), testStr) + + + def test_add_editor(self): + self.assertTrue(Editor.add(self.user1, self.item)) + self.assertEqual(len(self.item.editors()), 1) + self.assertTrue(self.user1 in self.item.editors()) + + self.assertFalse(Editor.add(self.user1, self.item)) + self.assertEqual(len(self.item.editors()), 1) + + self.assertTrue(Editor.add(self.user2, self.item)) + self.assertEqual(len(self.item.editors()), 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) + + + def test_remove_editor(self): + self.assertFalse(Editor.remove(self.user1, self.item)) + Editor.add(self.user1, self.item) + Editor.add(self.user2, self.item) + self.assertEqual(len(self.item.editors()), 2) + + self.assertTrue(Editor.remove(self.user1, self.item)) + self.assertEqual(len(self.item.editors()), 1) + self.assertTrue(self.user2 in self.item.editors()) + + self.assertFalse(Editor.remove(self.user1, self.item)) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py b/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py index 565d6dea..e0104842 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py @@ -12,16 +12,15 @@ class TestLibraryItem(TestCase): def setUp(self): self.user1 = User.objects.create(username='User1') self.user2 = User.objects.create(username='User2') - self.assertNotEqual(self.user1, self.user2) def test_str(self): testStr = 'Test123' item = LibraryItem.objects.create( item_type=LibraryItemType.RSFORM, - title=testStr, + title='Title', owner=self.user1, - alias='КС1' + alias=testStr ) self.assertEqual(str(item), testStr) @@ -64,37 +63,3 @@ class TestLibraryItem(TestCase): self.assertEqual(item.is_common, True) self.assertEqual(item.is_canonical, True) self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists()) - - - def test_subscribe(self): - item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') - self.assertEqual(len(item.subscribers()), 0) - - self.assertTrue(Subscription.subscribe(self.user1, item)) - self.assertEqual(len(item.subscribers()), 1) - self.assertTrue(self.user1 in item.subscribers()) - - self.assertFalse(Subscription.subscribe(self.user1, item)) - self.assertEqual(len(item.subscribers()), 1) - - self.assertTrue(Subscription.subscribe(self.user2, item)) - self.assertEqual(len(item.subscribers()), 2) - self.assertTrue(self.user1 in item.subscribers()) - self.assertTrue(self.user2 in item.subscribers()) - - self.user1.delete() - self.assertEqual(len(item.subscribers()), 1) - - - def test_unsubscribe(self): - item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') - self.assertFalse(Subscription.unsubscribe(self.user1, item)) - Subscription.subscribe(self.user1, item) - Subscription.subscribe(self.user2, item) - self.assertEqual(len(item.subscribers()), 2) - - self.assertTrue(Subscription.unsubscribe(self.user1, item)) - self.assertEqual(len(item.subscribers()), 1) - self.assertTrue(self.user2 in item.subscribers()) - - self.assertFalse(Subscription.unsubscribe(self.user1, item)) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py new file mode 100644 index 00000000..95ee8380 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py @@ -0,0 +1,68 @@ +''' Testing models: Subscription. ''' +from django.test import TestCase + +from apps.rsform.models import LibraryItem, LibraryItemType, Subscription, User + + +class TestSubscription(TestCase): + ''' Testing Subscription model. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + self.item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='КС1', + owner=self.user1 + ) + + + def test_default(self): + subs = list(Subscription.objects.filter(item=self.item)) + self.assertEqual(len(subs), 1) + self.assertEqual(subs[0].item, self.item) + self.assertEqual(subs[0].user, self.user1) + + + def test_str(self): + testStr = 'User2 -> КС1' + item = Subscription.objects.create( + user=self.user2, + item=self.item + ) + self.assertEqual(str(item), testStr) + + + def test_subscribe(self): + item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') + self.assertEqual(len(item.subscribers()), 0) + + self.assertTrue(Subscription.subscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + self.assertTrue(self.user1 in item.subscribers()) + + self.assertFalse(Subscription.subscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + + self.assertTrue(Subscription.subscribe(self.user2, item)) + self.assertEqual(len(item.subscribers()), 2) + self.assertTrue(self.user1 in item.subscribers()) + self.assertTrue(self.user2 in item.subscribers()) + + self.user1.delete() + self.assertEqual(len(item.subscribers()), 1) + + + def test_unsubscribe(self): + item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') + self.assertFalse(Subscription.unsubscribe(self.user1, item)) + Subscription.subscribe(self.user1, item) + Subscription.subscribe(self.user2, item) + self.assertEqual(len(item.subscribers()), 2) + + self.assertTrue(Subscription.unsubscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + self.assertTrue(self.user2 in item.subscribers()) + + self.assertFalse(Subscription.unsubscribe(self.user1, item)) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index 338b9142..93696aa2 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -104,6 +104,7 @@ class TestRSFormViewset(EndpointTester): self.assertEqual(response.data['items'][1]['term_raw'], x2.term_raw) self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved) self.assertEqual(response.data['subscribers'], [self.user.pk]) + self.assertEqual(response.data['editors'], []) @decl_endpoint('/api/rsforms/{item}/check', method='post') diff --git a/rsconcept/frontend/src/components/info/InfoLibraryItem.tsx b/rsconcept/frontend/src/components/info/InfoLibraryItem.tsx index 088b15ee..4beee062 100644 --- a/rsconcept/frontend/src/components/info/InfoLibraryItem.tsx +++ b/rsconcept/frontend/src/components/info/InfoLibraryItem.tsx @@ -15,6 +15,7 @@ function InfoLibraryItem({ item }: InfoLibraryItemProps) { return (
+ На всех активных элементах интерфейса при наведении отображаются контекстные подсказки. Некоторые элементы + интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала. +