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", "typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib",
"eslint.workingDirectories": ["rsconcept/frontend"], "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.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,

View File

@ -76,7 +76,7 @@ This readme file is used mostly to document project dependencies
<pre> <pre>
- Fira Code - Fira Code
- Rubik - Rubik
- Geologica - Alegreya Sans SC
- Noto Sans Math - Noto Sans Math
</pre> </pre>
</details> </details>
@ -115,6 +115,7 @@ This readme file is used mostly to document project dependencies
- Pylance - Pylance
- Pylint - Pylint
- Django - Django
- autopep8
</pre> </pre>
</details> </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 apps.users.models import User
from .Version import Version from .Version import Version
from .Subscription import Subscription from .Subscription import Subscription
from .Editor import Editor
class LibraryItemType(TextChoices): class LibraryItemType(TextChoices):
@ -64,7 +65,7 @@ class LibraryItem(Model):
verbose_name_plural = 'Схемы' verbose_name_plural = 'Схемы'
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.title}' return f'{self.alias}'
def get_absolute_url(self): def get_absolute_url(self):
return f'/api/library/{self.pk}' return f'/api/library/{self.pk}'
@ -77,6 +78,10 @@ class LibraryItem(Model):
''' Get all Versions of this item. ''' ''' Get all Versions of this item. '''
return list(Version.objects.filter(item=self.pk).order_by('-time_create')) 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 @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
subscribe = not self.pk and self.owner subscribe = not self.pk and self.owner

View File

@ -25,8 +25,8 @@ class Subscription(Model):
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''
verbose_name = 'Подписки' verbose_name = 'Подписка'
verbose_name_plural = 'Подписка' verbose_name_plural = 'Подписки'
unique_together = [['user', 'item']] unique_together = [['user', 'item']]
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -31,8 +31,8 @@ class Version(Model):
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''
verbose_name = 'Версии' verbose_name = 'Версия'
verbose_name_plural = 'Версия' verbose_name_plural = 'Версии'
unique_together = [['item', 'version']] unique_together = [['item', 'version']]
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -5,4 +5,5 @@ from .Constituenta import Constituenta, CstType, _empty_forms
from .LibraryItem import User, LibraryItem, LibraryItemType from .LibraryItem import User, LibraryItem, LibraryItemType
from .LibraryTemplate import LibraryTemplate from .LibraryTemplate import LibraryTemplate
from .Version import Version from .Version import Version
from .Editor import Editor
from .Subscription import Subscription from .Subscription import Subscription

View File

@ -52,6 +52,7 @@ class VersionCreateSerializer(serializers.ModelSerializer):
class LibraryItemDetailsSerializer(serializers.ModelSerializer): class LibraryItemDetailsSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem detailed data. ''' ''' Serializer: LibraryItem detailed data. '''
subscribers = serializers.SerializerMethodField() subscribers = serializers.SerializerMethodField()
editors = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField() versions = serializers.SerializerMethodField()
class Meta: class Meta:
@ -63,6 +64,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
def get_subscribers(self, instance: LibraryItem) -> list[int]: def get_subscribers(self, instance: LibraryItem) -> list[int]:
return [item.pk for item in instance.subscribers()] 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: def get_versions(self, instance: LibraryItem) -> list:
return [VersionInnerSerializer(item).data for item in instance.versions()] return [VersionInnerSerializer(item).data for item in instance.versions()]
@ -133,6 +137,9 @@ class RSFormSerializer(serializers.ModelSerializer):
subscribers = serializers.ListField( subscribers = serializers.ListField(
child=serializers.IntegerField() child=serializers.IntegerField()
) )
editors = serializers.ListField(
child=serializers.IntegerField()
)
items = serializers.ListField( items = serializers.ListField(
child=CstSerializer() child=CstSerializer()
) )
@ -155,6 +162,7 @@ class RSFormSerializer(serializers.ModelSerializer):
result = self.to_representation(cast(LibraryItem, self.instance)) result = self.to_representation(cast(LibraryItem, self.instance))
del result['versions'] del result['versions']
del result['subscribers'] del result['subscribers']
del result['editors']
del result['owner'] del result['owner']
del result['is_common'] del result['is_common']
@ -214,6 +222,9 @@ class RSFormParseSerializer(serializers.ModelSerializer):
subscribers = serializers.ListField( subscribers = serializers.ListField(
child=serializers.IntegerField() child=serializers.IntegerField()
) )
editors = serializers.ListField(
child=serializers.IntegerField()
)
items = serializers.ListField( items = serializers.ListField(
child=CstDetailsSerializer() child=CstDetailsSerializer()
) )

View File

@ -1,4 +1,6 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_Constituenta import * from .t_Constituenta import *
from .t_LibraryItem import * from .t_LibraryItem import *
from .t_Editor import *
from .t_Subscription import *
from .t_RSForm 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): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
self.assertNotEqual(self.user1, self.user2)
def test_str(self): def test_str(self):
testStr = 'Test123' testStr = 'Test123'
item = LibraryItem.objects.create( item = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM, item_type=LibraryItemType.RSFORM,
title=testStr, title='Title',
owner=self.user1, owner=self.user1,
alias='КС1' alias=testStr
) )
self.assertEqual(str(item), testStr) self.assertEqual(str(item), testStr)
@ -64,37 +63,3 @@ class TestLibraryItem(TestCase):
self.assertEqual(item.is_common, True) self.assertEqual(item.is_common, True)
self.assertEqual(item.is_canonical, True) self.assertEqual(item.is_canonical, True)
self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists()) 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_raw'], x2.term_raw)
self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved) self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved)
self.assertEqual(response.data['subscribers'], [self.user.pk]) self.assertEqual(response.data['subscribers'], [self.user.pk])
self.assertEqual(response.data['editors'], [])
@decl_endpoint('/api/rsforms/{item}/check', method='post') @decl_endpoint('/api/rsforms/{item}/check', method='post')

View File

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

View File

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

View File

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