mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
Add Editors to database
This commit is contained in:
parent
1a210606d7
commit
f669a3054b
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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': 'Версии'},
|
||||
),
|
||||
]
|
30
rsconcept/backend/apps/rsform/migrations/0006_editor.py
Normal file
30
rsconcept/backend/apps/rsform/migrations/0006_editor.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
58
rsconcept/backend/apps/rsform/models/Editor.py
Normal file
58
rsconcept/backend/apps/rsform/models/Editor.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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 *
|
||||
|
|
62
rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py
Normal file
62
rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py
Normal 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))
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
|
@ -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')
|
||||
|
|
|
@ -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='Дата обновления'
|
||||
|
|
|
@ -127,6 +127,7 @@ export interface ILibraryItem {
|
|||
*/
|
||||
export interface ILibraryItemEx extends ILibraryItem {
|
||||
subscribers: number[];
|
||||
editors: number[];
|
||||
version?: number;
|
||||
versions: IVersionInfo[];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ function HelpInterface() {
|
|||
Интерфейс построен на основе динамических компонент с использованием рендеринга графики в браузере.
|
||||
Поддерживаются светлая и темная темы оформления.
|
||||
</p>
|
||||
<p>
|
||||
На всех активных элементах интерфейса при наведении отображаются контекстные подсказки. Некоторые элементы
|
||||
интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала.
|
||||
</p>
|
||||
<p>
|
||||
<IconHelp className='inline-icon' />
|
||||
Помимо данного раздела справка предоставляется контекстно через специальную иконку{' '}
|
||||
|
|
Loading…
Reference in New Issue
Block a user