Add Editors to database

This commit is contained in:
IRBorisov 2024-05-24 15:40:28 +03:00
parent 1a210606d7
commit f669a3054b
18 changed files with 280 additions and 43 deletions

View File

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

View File

@ -76,7 +76,7 @@ This readme file is used mostly to document project dependencies
<pre>
- Fira Code
- Rubik
- Geologica
- Alegreya Sans SC
- Noto Sans Math
</pre>
</details>
@ -115,6 +115,7 @@ This readme file is used mostly to document project dependencies
- Pylance
- Pylint
- Django
- autopep8
</pre>
</details>

View File

@ -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': 'Версии'},
),
]

View File

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

View File

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

View File

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

View File

@ -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:

View File

@ -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:

View File

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

View File

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

View File

@ -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 *

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ function InfoLibraryItem({ item }: InfoLibraryItemProps) {
return (
<div className='flex flex-col gap-1'>
<LabeledValue label='Владелец' text={getUserLabel(item?.owner ?? null)} />
<LabeledValue label='Редакторы' text={item?.editors.length ?? 0} />
<LabeledValue label='Отслеживают' text={item?.subscribers.length ?? 0} />
<LabeledValue
label='Дата обновления'

View File

@ -127,6 +127,7 @@ export interface ILibraryItem {
*/
export interface ILibraryItemEx extends ILibraryItem {
subscribers: number[];
editors: number[];
version?: number;
versions: IVersionInfo[];
}

View File

@ -21,6 +21,10 @@ function HelpInterface() {
Интерфейс построен на основе динамических компонент с использованием рендеринга графики в браузере.
Поддерживаются светлая и темная темы оформления.
</p>
<p>
На всех активных элементах интерфейса при наведении отображаются контекстные подсказки. Некоторые элементы
интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала.
</p>
<p>
<IconHelp className='inline-icon' />
Помимо данного раздела справка предоставляется контекстно через специальную иконку{' '}