mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add library template support for backend
This commit is contained in:
parent
9cd682b19f
commit
72039727ff
|
@ -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)
|
||||||
|
|
|
@ -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': 'Шаблоны',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user