Add library template support for backend

This commit is contained in:
IRBorisov 2023-10-23 20:55:12 +03:00
parent 9cd682b19f
commit 72039727ff
8 changed files with 128 additions and 21 deletions

View File

@ -10,7 +10,8 @@ class ConstituentaAdmin(admin.ModelAdmin):
list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved'] list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved']
search_fields = ['term_resolved', 'definition_resolved'] search_fields = ['term_resolved', 'definition_resolved']
class LibraryAdmin(admin.ModelAdmin):
class LibraryItemAdmin(admin.ModelAdmin):
''' Admin model: LibraryItem. ''' ''' Admin model: LibraryItem. '''
date_hierarchy = 'time_update' date_hierarchy = 'time_update'
list_display = [ list_display = [
@ -22,6 +23,18 @@ class LibraryAdmin(admin.ModelAdmin):
search_fields = ['alias', 'title'] search_fields = ['alias', 'title']
class LibraryTemplateAdmin(admin.ModelAdmin):
''' Admin model: LibraryTemplate. '''
list_display = ['id', 'alias']
list_select_related = ['lib_source']
def alias(self, template: models.LibraryTemplate):
if template.lib_source:
return template.lib_source.alias
else:
return 'N/A'
class SubscriptionAdmin(admin.ModelAdmin): class SubscriptionAdmin(admin.ModelAdmin):
''' Admin model: Subscriptions. ''' ''' Admin model: Subscriptions. '''
list_display = ['id', 'item', 'user'] list_display = ['id', 'item', 'user']
@ -32,5 +45,6 @@ class SubscriptionAdmin(admin.ModelAdmin):
admin.site.register(models.Constituenta, ConstituentaAdmin) admin.site.register(models.Constituenta, ConstituentaAdmin)
admin.site.register(models.LibraryItem, LibraryAdmin) admin.site.register(models.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
admin.site.register(models.Subscription, SubscriptionAdmin) admin.site.register(models.Subscription, SubscriptionAdmin)

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-10-18 16:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('rsform', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LibraryTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lib_source', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Источник')),
],
options={
'verbose_name': 'Шаблон',
'verbose_name_plural': 'Шаблоны',
},
),
]

View File

@ -71,8 +71,7 @@ def _get_type_prefix(cst_type: CstType) -> str:
class LibraryItem(Model): class LibraryItem(Model):
''' Abstract library item. ''' Abstract library item.'''
Please use wrappers below to access functionality. '''
item_type: CharField = CharField( item_type: CharField = CharField(
verbose_name='Тип', verbose_name='Тип',
max_length=50, max_length=50,
@ -136,6 +135,21 @@ class LibraryItem(Model):
Subscription.subscribe(user=self.owner, item=self) Subscription.subscribe(user=self.owner, item=self)
class LibraryTemplate(Model):
''' Template for library items and constituents. '''
lib_source: ForeignKey = ForeignKey(
verbose_name='Источник',
to=LibraryItem,
on_delete=CASCADE,
null=True
)
class Meta:
''' Model metadata. '''
verbose_name = 'Шаблон'
verbose_name_plural = 'Шаблоны'
class Subscription(Model): class Subscription(Model):
''' User subscription to library item. ''' ''' User subscription to library item. '''
user: ForeignKey = ForeignKey( user: ForeignKey = ForeignKey(
@ -319,6 +333,8 @@ class RSForm:
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
if position <= 0: if position <= 0:
raise ValidationError('Invalid position: should be positive integer') raise ValidationError('Invalid position: should be positive integer')
if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}')
currentSize = self.constituents().count() currentSize = self.constituents().count()
position = max(1, min(position, currentSize + 1)) position = max(1, min(position, currentSize + 1))
update_list = \ update_list = \
@ -342,6 +358,8 @@ class RSForm:
@transaction.atomic @transaction.atomic
def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta':
''' Insert new constituenta at last position ''' ''' Insert new constituenta at last position '''
if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}')
position = 1 position = 1
if self.constituents().exists(): if self.constituents().exists():
position += self.constituents().count() position += self.constituents().count()

View File

@ -188,13 +188,18 @@ class CstRenameSerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
schema = cast(RSForm, self.context['schema']) schema = cast(RSForm, self.context['schema'])
old_cst = Constituenta.objects.get(pk=self.initial_data['id']) old_cst = Constituenta.objects.get(pk=self.initial_data['id'])
new_alias = self.initial_data['alias']
if old_cst.schema != schema.item: if old_cst.schema != schema.item:
raise serializers.ValidationError({ raise serializers.ValidationError({
'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}' 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}'
}) })
if old_cst.alias == self.initial_data['alias']: if old_cst.alias == new_alias:
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}' 'alias': f'Имя конституенты должно отличаться от текущего: {new_alias}'
})
if schema.constituents().filter(alias=new_alias).exists():
raise serializers.ValidationError({
'alias': f'Конституента с таким именем уже существует: {new_alias}'
}) })
self.instance = old_cst self.instance = old_cst
attrs['schema'] = schema.item attrs['schema'] = schema.item

View File

@ -190,9 +190,17 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
def test_insert_at_invalid_position(self):
schema = RSForm.create(title='Test')
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
schema.insert_at(0, 'X5', CstType.BASE) schema.insert_at(0, 'X5', CstType.BASE)
def test_insert_at_invalid_alias(self):
schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE)
with self.assertRaises(ValidationError):
schema.insert_at(2, 'X1', CstType.BASE)
def test_insert_at_reorder(self): def test_insert_at_reorder(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)

View File

@ -8,7 +8,10 @@ from rest_framework.exceptions import ErrorDetail
from cctext import ReferenceType, split_grams from cctext import ReferenceType, split_grams
from apps.users.models import User from apps.users.models import User
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType, LibraryItem, LibraryItemType, Subscription from apps.rsform.models import (
Syntax, RSForm, Constituenta, CstType,
LibraryItem, LibraryItemType, Subscription, LibraryTemplate
)
from apps.rsform.views import ( from apps.rsform.views import (
convert_to_ascii, convert_to_ascii,
convert_to_math, convert_to_math,
@ -264,6 +267,20 @@ class TestLibraryViewset(APITestCase):
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertFalse(self.user in self.unowned.subscribers()) self.assertFalse(self.user in self.unowned.subscribers())
def test_retrieve_templates(self):
response = self.client.get('/api/library/templates')
self.assertEqual(response.status_code, 200)
self.assertFalse(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned))
LibraryTemplate.objects.create(lib_source=self.unowned)
response = self.client.get('/api/library/templates')
self.assertEqual(response.status_code, 200)
self.assertFalse(_response_contains(response, self.common))
self.assertTrue(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned))
class TestRSFormViewset(APITestCase): class TestRSFormViewset(APITestCase):
''' Testing RSForm view. ''' ''' Testing RSForm view. '''
@ -420,22 +437,22 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
def test_rename_constituenta(self): def test_rename_constituenta(self):
self.cst1 = Constituenta.objects.create( cst1 = Constituenta.objects.create(
alias='X1', schema=self.owned.item, order=1, convention='Test', alias='X1', schema=self.owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1', term_raw='Test1', term_resolved='Test1',
term_forms=[{'text':'form1', 'tags':'sing,datv'}] term_forms=[{'text':'form1', 'tags':'sing,datv'}]
) )
self.cst2 = Constituenta.objects.create( cst2 = Constituenta.objects.create(
alias='X2', schema=self.unowned.item, order=1, convention='Test1', alias='X2', schema=self.unowned.item, order=1, convention='Test1',
term_raw='Test2', term_resolved='Test2' term_raw='Test2', term_resolved='Test2'
) )
self.cst3 = Constituenta.objects.create( cst3 = Constituenta.objects.create(
alias='X3', schema=self.owned.item, order=2, alias='X3', schema=self.owned.item, order=2,
term_raw='Test3', term_resolved='Test3', term_raw='Test3', term_resolved='Test3',
definition_raw='Test1', definition_resolved='Test2' definition_raw='Test1', definition_resolved='Test2'
) )
data = {'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk} data = {'alias': 'D2', 'cst_type': 'term', 'id': cst2.pk}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.unowned.item.id}/cst-rename', f'/api/rsforms/{self.unowned.item.id}/cst-rename',
data=data, format='json' data=data, format='json'
@ -448,14 +465,21 @@ class TestRSFormViewset(APITestCase):
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = {'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk} data = {'alias': cst1.alias, 'cst_type': 'term', 'id': cst1.pk}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename', f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = {'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk} data = {'alias': cst3.alias, 'id': cst1.pk}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'alias': 'D2', 'cst_type': 'term', 'id': cst1.pk}
item = self.owned.item item = self.owned.item
d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4) d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4)
d1.term_raw = '@{X1|plur}' d1.term_raw = '@{X1|plur}'
@ -463,9 +487,9 @@ class TestRSFormViewset(APITestCase):
d1.save() d1.save()
self.assertEqual(d1.order, 4) self.assertEqual(d1.order, 4)
self.assertEqual(self.cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(self.cst1.alias, 'X1') self.assertEqual(cst1.alias, 'X1')
self.assertEqual(self.cst1.cst_type, CstType.BASE) self.assertEqual(cst1.cst_type, CstType.BASE)
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{item.id}/cst-rename', f'/api/rsforms/{item.id}/cst-rename',
data=data, format='json' data=data, format='json'
@ -474,13 +498,13 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['new_cst']['alias'], 'D2') self.assertEqual(response.data['new_cst']['alias'], 'D2')
self.assertEqual(response.data['new_cst']['cst_type'], 'term') self.assertEqual(response.data['new_cst']['cst_type'], 'term')
d1.refresh_from_db() d1.refresh_from_db()
self.cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(d1.order, 4) self.assertEqual(d1.order, 4)
self.assertEqual(d1.term_resolved, '') self.assertEqual(d1.term_resolved, '')
self.assertEqual(d1.term_raw, '@{D2|plur}') self.assertEqual(d1.term_raw, '@{D2|plur}')
self.assertEqual(self.cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(self.cst1.alias, 'D2') self.assertEqual(cst1.alias, 'D2')
self.assertEqual(self.cst1.cst_type, CstType.TERM) self.assertEqual(cst1.cst_type, CstType.TERM)
def test_create_constituenta_data(self): def test_create_constituenta_data(self):
data = { data = {

View File

@ -9,6 +9,7 @@ library_router.register('rsforms', views.RSFormViewSet)
urlpatterns = [ urlpatterns = [
path('library/active', views.LibraryActiveView.as_view(), name='library'), path('library/active', views.LibraryActiveView.as_view(), name='library'),
path('library/templates', views.LibraryTemplatesView.as_view(), name='library'),
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'), path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs', views.TrsImportView.as_view()), path('rsforms/import-trs', views.TrsImportView.as_view()),
path('rsforms/create-detailed', views.create_rsform), path('rsforms/create-detailed', views.create_rsform),

View File

@ -22,7 +22,7 @@ from . import utils
@extend_schema(tags=['Library']) @extend_schema(tags=['Library'])
@extend_schema_view() @extend_schema_view()
class LibraryActiveView(generics.ListAPIView): class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of rsforms available for active user. ''' ''' Endpoint: Get list of library items available for active user. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
@ -37,6 +37,18 @@ class LibraryActiveView(generics.ListAPIView):
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update') return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryTemplatesView(generics.ListAPIView):
''' Endpoint: Get list of templates. '''
permission_classes = (permissions.AllowAny,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True)
return m.LibraryItem.objects.filter(pk__in=template_ids)
@extend_schema(tags=['Constituenta']) @extend_schema(tags=['Constituenta'])
@extend_schema_view() @extend_schema_view()
class ConstituentAPIView(generics.RetrieveUpdateAPIView): class ConstituentAPIView(generics.RetrieveUpdateAPIView):