Implementing versioning: backend createVersion

This commit is contained in:
IRBorisov 2024-03-03 22:00:22 +03:00
parent e0deefd0cb
commit ca4882575e
13 changed files with 377 additions and 144 deletions

View File

@ -34,3 +34,4 @@ DB_PASSWORD=78ACF6C4F3
DEBUG=1 DEBUG=1
PYTHONDEVMODE=1 PYTHONDEVMODE=1
PYTHONTRACEMALLOC=1 PYTHONTRACEMALLOC=1
DJANGO_LOG_LEVEL=DEBUG

View File

@ -34,3 +34,4 @@ DB_PORT=5432
DEBUG=0 DEBUG=0
PYTHONDEVMODE=0 PYTHONDEVMODE=0
PYTHONTRACEMALLOC=0 PYTHONTRACEMALLOC=0
DJANGO_LOG_LEVEL=DEBUG

View File

@ -34,3 +34,4 @@ DB_PASSWORD=78ACF6C4F3
DEBUG=0 DEBUG=0
PYTHONDEVMODE=0 PYTHONDEVMODE=0
PYTHONTRACEMALLOC=0 PYTHONTRACEMALLOC=0
DJANGO_LOG_LEVEL=DEBUG

View File

@ -44,7 +44,16 @@ class SubscriptionAdmin(admin.ModelAdmin):
] ]
class VersionAdmin(admin.ModelAdmin):
''' Admin model: Versions. '''
list_display = ['id', 'item', 'version', 'description', 'time_create']
search_fields = [
'item__title', 'item__alias'
]
admin.site.register(models.Constituenta, ConstituentaAdmin) admin.site.register(models.Constituenta, ConstituentaAdmin)
admin.site.register(models.LibraryItem, LibraryItemAdmin) admin.site.register(models.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin) admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
admin.site.register(models.Subscription, SubscriptionAdmin) admin.site.register(models.Subscription, SubscriptionAdmin)
admin.site.register(models.Version, VersionAdmin)

View File

@ -0,0 +1,30 @@
# Generated by Django 4.2.10 on 2024-03-03 10:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('rsform', '0003_alter_constituenta_definition_raw_and_more'),
]
operations = [
migrations.CreateModel(
name='Version',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.CharField(max_length=20, verbose_name='Версия')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('data', models.JSONField(verbose_name='Содержание')),
('time_create', models.DateTimeField(auto_now_add=True, 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', 'version')},
},
),
]

View File

@ -128,6 +128,10 @@ class LibraryItem(Model):
''' Get all subscribers for this item. ''' ''' Get all subscribers for this item. '''
return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)] return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)]
def versions(self) -> list['Version']:
''' Get all Versions of this item. '''
return list(Version.objects.filter(item=self.pk))
@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
@ -151,6 +155,40 @@ class LibraryTemplate(Model):
verbose_name_plural = 'Шаблоны' verbose_name_plural = 'Шаблоны'
class Version(Model):
''' Library item version archive. '''
item: ForeignKey = ForeignKey(
verbose_name='Схема',
to=LibraryItem,
on_delete=CASCADE
)
version = CharField(
verbose_name='Версия',
max_length=20,
blank=False
)
description: TextField = TextField(
verbose_name='Описание',
blank=True
)
data: JSONField = JSONField(
verbose_name='Содержание'
)
time_create: DateTimeField = DateTimeField(
verbose_name='Дата создания',
auto_now_add=True
)
class Meta:
''' Model metadata. '''
verbose_name = 'Версии'
verbose_name_plural = 'Версия'
unique_together = [['item', 'version']]
def __str__(self) -> str:
return f'{self.item} v{self.version}'
class Subscription(Model): class Subscription(Model):
''' User subscription to library item. ''' ''' User subscription to library item. '''
user: ForeignKey = ForeignKey( user: ForeignKey = ForeignKey(
@ -511,6 +549,16 @@ class RSForm:
cst.definition_resolved = resolved cst.definition_resolved = resolved
cst.save() cst.save()
@transaction.atomic
def create_version(self, version: str, description: str, data) -> Version:
''' Creates version for current state. '''
return Version.objects.create(
item=self.item,
version=version,
description=description,
data=data
)
@transaction.atomic @transaction.atomic
def _reset_order(self): def _reset_order(self):
order = 1 order = 1

View File

@ -8,7 +8,7 @@ import pyconcept
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
from .utils import fix_old_references from .utils import fix_old_references
from .models import Constituenta, LibraryItem, RSForm from .models import Constituenta, LibraryItem, RSForm, Version
from . import messages as msg from . import messages as msg
_CST_TYPE = 'constituenta' _CST_TYPE = 'constituenta'
@ -128,9 +128,27 @@ class ExpressionParseSerializer(serializers.Serializer):
) )
class VersionSerializer(serializers.ModelSerializer):
''' Serializer: Version data. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'id', 'version', 'description', 'time_create'
read_only_fields = ('item', 'id', 'time_create')
class VersionCreateSerializer(serializers.ModelSerializer):
''' Serializer: Version create data. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'version', 'description'
class LibraryItemDetailsSerializer(serializers.ModelSerializer): class LibraryItemDetailsSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem detailed data. ''' ''' Serializer: LibraryItem detailed data. '''
subscribers = serializers.SerializerMethodField() subscribers = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField()
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -141,6 +159,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_versions(self, instance: LibraryItem) -> list:
return [VersionSerializer(item).data for item in instance.versions()]
class ConstituentaSerializer(serializers.ModelSerializer): class ConstituentaSerializer(serializers.ModelSerializer):
''' Serializer: Constituenta data. ''' ''' Serializer: Constituenta data. '''
@ -398,7 +419,7 @@ class RSFormSerializer(serializers.ModelSerializer):
model = LibraryItem model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: LibraryItem): def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
schema = RSForm(instance) schema = RSForm(instance)
result['items'] = [] result['items'] = []
@ -406,6 +427,25 @@ class RSFormSerializer(serializers.ModelSerializer):
result['items'].append(ConstituentaSerializer(cst).data) result['items'].append(ConstituentaSerializer(cst).data)
return result return result
def to_versioned_data(self) -> dict:
''' Create serializable version representation without redundant data. '''
result = self.to_representation(self.instance)
del result['versions']
del result['subscribers']
del result['owner']
del result['is_common']
del result['is_canonical']
del result['time_create']
del result['time_update']
return result
def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. '''
result = self.to_representation(self.instance)
result['version'] = version
return result | data
class CstDetailsSerializer(serializers.ModelSerializer): class CstDetailsSerializer(serializers.ModelSerializer):
''' Serializer: Constituenta data including parse. ''' ''' Serializer: Constituenta data including parse. '''

View File

@ -4,6 +4,7 @@ import io
from zipfile import ZipFile from zipfile import ZipFile
from rest_framework.test import APITestCase, APIRequestFactory, APIClient from rest_framework.test import APITestCase, APIRequestFactory, APIClient
from rest_framework.exceptions import ErrorDetail from rest_framework.exceptions import ErrorDetail
from rest_framework import status
from cctext import ReferenceType, split_grams from cctext import ReferenceType, split_grams
@ -63,7 +64,7 @@ class TestConstituentaAPI(APITestCase):
def test_retrieve(self): def test_retrieve(self):
response = self.client.get(f'/api/constituents/{self.cst1.id}') response = self.client.get(f'/api/constituents/{self.cst1.id}')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['alias'], self.cst1.alias) self.assertEqual(response.data['alias'], self.cst1.alias)
self.assertEqual(response.data['convention'], self.cst1.convention) self.assertEqual(response.data['convention'], self.cst1.convention)
@ -73,21 +74,21 @@ class TestConstituentaAPI(APITestCase):
f'/api/constituents/{self.cst2.id}', f'/api/constituents/{self.cst2.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.logout() self.client.logout()
response = self.client.patch( response = self.client.patch(
f'/api/constituents/{self.cst1.id}', f'/api/constituents/{self.cst1.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
response = self.client.patch( response = self.client.patch(
f'/api/constituents/{self.cst1.id}', f'/api/constituents/{self.cst1.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.cst1.refresh_from_db() self.cst1.refresh_from_db()
self.assertEqual(response.data['convention'], 'tt') self.assertEqual(response.data['convention'], 'tt')
self.assertEqual(self.cst1.convention, 'tt') self.assertEqual(self.cst1.convention, 'tt')
@ -97,7 +98,7 @@ class TestConstituentaAPI(APITestCase):
data=data, data=data,
format='json' format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_update_resolved_no_refs(self): def test_update_resolved_no_refs(self):
data = { data = {
@ -105,7 +106,7 @@ class TestConstituentaAPI(APITestCase):
'definition_raw': 'New def' 'definition_raw': 'New def'
} }
response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, format='json') response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, format='json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.cst3.refresh_from_db() self.cst3.refresh_from_db()
self.assertEqual(response.data['term_resolved'], 'New term') self.assertEqual(response.data['term_resolved'], 'New term')
self.assertEqual(self.cst3.term_resolved, 'New term') self.assertEqual(self.cst3.term_resolved, 'New term')
@ -121,7 +122,7 @@ class TestConstituentaAPI(APITestCase):
f'/api/constituents/{self.cst3.id}', f'/api/constituents/{self.cst3.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.cst3.refresh_from_db() self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved) self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved)
@ -134,7 +135,7 @@ class TestConstituentaAPI(APITestCase):
f'/api/constituents/{self.cst1.id}', f'/api/constituents/{self.cst1.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['alias'], 'X1') self.assertEqual(response.data['alias'], 'X1')
self.assertEqual(response.data['alias'], self.cst1.alias) self.assertEqual(response.data['alias'], self.cst1.alias)
self.assertEqual(response.data['order'], self.cst1.order) self.assertEqual(response.data['order'], self.cst1.order)
@ -169,12 +170,12 @@ class TestLibraryViewset(APITestCase):
self.client.logout() self.client.logout()
data = {'title': 'Title'} data = {'title': 'Title'}
response = self.client.post('/api/library', data=data, format='json') response = self.client.post('/api/library', data=data, format='json')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_create_populate_user(self): def test_create_populate_user(self):
data = {'title': 'Title'} data = {'title': 'Title'}
response = self.client.post('/api/library', data=data, format='json') response = self.client.post('/api/library', data=data, format='json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['owner'], self.user.id) self.assertEqual(response.data['owner'], self.user.id)
@ -184,7 +185,7 @@ class TestLibraryViewset(APITestCase):
f'/api/library/{self.owned.id}', f'/api/library/{self.owned.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'New title') self.assertEqual(response.data['title'], 'New title')
self.assertEqual(response.data['alias'], self.owned.alias) self.assertEqual(response.data['alias'], self.owned.alias)
@ -194,37 +195,37 @@ class TestLibraryViewset(APITestCase):
f'/api/library/{self.unowned.id}', f'/api/library/{self.unowned.id}',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_destroy(self): def test_destroy(self):
response = self.client.delete(f'/api/library/{self.owned.id}') response = self.client.delete(f'/api/library/{self.owned.id}')
self.assertTrue(response.status_code in [202, 204]) self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
def test_destroy_admin_override(self): def test_destroy_admin_override(self):
response = self.client.delete(f'/api/library/{self.unowned.id}') response = self.client.delete(f'/api/library/{self.unowned.id}')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.user.is_staff = True self.user.is_staff = True
self.user.save() self.user.save()
response = self.client.delete(f'/api/library/{self.unowned.id}') response = self.client.delete(f'/api/library/{self.unowned.id}')
self.assertTrue(response.status_code in [202, 204]) self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
def test_claim(self): def test_claim(self):
response = self.client.post(f'/api/library/{self.owned.id}/claim') response = self.client.post(f'/api/library/{self.owned.id}/claim')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.owned.is_common = True self.owned.is_common = True
self.owned.save() self.owned.save()
response = self.client.post(f'/api/library/{self.owned.id}/claim') response = self.client.post(f'/api/library/{self.owned.id}/claim')
self.assertEqual(response.status_code, 304) self.assertEqual(response.status_code, status.HTTP_304_NOT_MODIFIED)
response = self.client.post(f'/api/library/{self.unowned.id}/claim') response = self.client.post(f'/api/library/{self.unowned.id}/claim')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(self.user in self.unowned.subscribers()) self.assertFalse(self.user in self.unowned.subscribers())
self.unowned.is_common = True self.unowned.is_common = True
self.unowned.save() self.unowned.save()
response = self.client.post(f'/api/library/{self.unowned.id}/claim') response = self.client.post(f'/api/library/{self.unowned.id}/claim')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.unowned.refresh_from_db() self.unowned.refresh_from_db()
self.assertEqual(self.unowned.owner, self.user) self.assertEqual(self.unowned.owner, self.user)
self.assertEqual(self.unowned.owner, self.user) self.assertEqual(self.unowned.owner, self.user)
@ -233,26 +234,26 @@ class TestLibraryViewset(APITestCase):
def test_claim_anonymous(self): def test_claim_anonymous(self):
self.client.logout() self.client.logout()
response = self.client.post(f'/api/library/{self.owned.id}/claim') response = self.client.post(f'/api/library/{self.owned.id}/claim')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_retrieve_common(self): def test_retrieve_common(self):
self.client.logout() self.client.logout()
response = self.client.get('/api/library/active') response = self.client.get('/api/library/active')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(_response_contains(response, self.common)) self.assertTrue(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned)) self.assertFalse(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned)) self.assertFalse(_response_contains(response, self.owned))
def test_retrieve_owned(self): def test_retrieve_owned(self):
response = self.client.get('/api/library/active') response = self.client.get('/api/library/active')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(_response_contains(response, self.common)) self.assertTrue(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned)) self.assertFalse(_response_contains(response, self.unowned))
self.assertTrue(_response_contains(response, self.owned)) self.assertTrue(_response_contains(response, self.owned))
def test_retrieve_subscribed(self): def test_retrieve_subscribed(self):
response = self.client.get('/api/library/active') response = self.client.get('/api/library/active')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(_response_contains(response, self.unowned)) self.assertFalse(_response_contains(response, self.unowned))
user2 = User.objects.create(username='UserTest2') user2 = User.objects.create(username='UserTest2')
@ -260,37 +261,37 @@ class TestLibraryViewset(APITestCase):
Subscription.subscribe(user=user2, item=self.unowned) Subscription.subscribe(user=user2, item=self.unowned)
Subscription.subscribe(user=user2, item=self.owned) Subscription.subscribe(user=user2, item=self.owned)
response = self.client.get('/api/library/active') response = self.client.get('/api/library/active')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(_response_contains(response, self.unowned)) self.assertTrue(_response_contains(response, self.unowned))
self.assertEqual(len(response.data), 3) self.assertEqual(len(response.data), 3)
def test_subscriptions(self): def test_subscriptions(self):
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe') response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(self.user in self.unowned.subscribers()) self.assertFalse(self.user in self.unowned.subscribers())
response = self.client.post(f'/api/library/{self.unowned.id}/subscribe') response = self.client.post(f'/api/library/{self.unowned.id}/subscribe')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertTrue(self.user in self.unowned.subscribers()) self.assertTrue(self.user in self.unowned.subscribers())
response = self.client.post(f'/api/library/{self.unowned.id}/subscribe') response = self.client.post(f'/api/library/{self.unowned.id}/subscribe')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertTrue(self.user in self.unowned.subscribers()) self.assertTrue(self.user in self.unowned.subscribers())
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe') response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(self.user in self.unowned.subscribers()) self.assertFalse(self.user in self.unowned.subscribers())
def test_retrieve_templates(self): def test_retrieve_templates(self):
response = self.client.get('/api/library/templates') response = self.client.get('/api/library/templates')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(_response_contains(response, self.common)) self.assertFalse(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned)) self.assertFalse(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned)) self.assertFalse(_response_contains(response, self.owned))
LibraryTemplate.objects.create(lib_source=self.unowned) LibraryTemplate.objects.create(lib_source=self.unowned)
response = self.client.get('/api/library/templates') response = self.client.get('/api/library/templates')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(_response_contains(response, self.common)) self.assertFalse(_response_contains(response, self.common))
self.assertTrue(_response_contains(response, self.unowned)) self.assertTrue(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned)) self.assertFalse(_response_contains(response, self.owned))
@ -312,13 +313,13 @@ class TestRSFormViewset(APITestCase):
title='Test3' title='Test3'
) )
response = self.client.get('/api/rsforms') response = self.client.get('/api/rsforms')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(_response_contains(response, non_schema)) self.assertFalse(_response_contains(response, non_schema))
self.assertTrue(_response_contains(response, self.unowned.item)) self.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item)) self.assertTrue(_response_contains(response, self.owned.item))
response = self.client.get('/api/library') response = self.client.get('/api/library')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(_response_contains(response, non_schema)) self.assertTrue(_response_contains(response, non_schema))
self.assertTrue(_response_contains(response, self.unowned.item)) self.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item)) self.assertTrue(_response_contains(response, self.owned.item))
@ -327,7 +328,7 @@ class TestRSFormViewset(APITestCase):
schema = RSForm.create(title='Title1') schema = RSForm.create(title='Title1')
schema.insert_last(alias='X1', insert_type=CstType.BASE) schema.insert_last(alias='X1', insert_type=CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.item.id}/contents') response = self.client.get(f'/api/rsforms/{schema.item.id}/contents')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_details(self): def test_details(self):
schema = RSForm.create(title='Test', owner=self.user) schema = RSForm.create(title='Test', owner=self.user)
@ -342,7 +343,7 @@ class TestRSFormViewset(APITestCase):
response = self.client.get(f'/api/rsforms/{schema.item.id}/details') response = self.client.get(f'/api/rsforms/{schema.item.id}/details')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test') self.assertEqual(response.data['title'], 'Test')
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.id) self.assertEqual(response.data['items'][0]['id'], x1.id)
@ -362,7 +363,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/check', f'/api/rsforms/{schema.item.id}/check',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], Syntax.MATH) self.assertEqual(response.data['syntax'], Syntax.MATH)
self.assertEqual(response.data['astText'], '[=[X1][X1]]') self.assertEqual(response.data['astText'], '[=[X1][X1]]')
@ -373,7 +374,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/check', f'/api/rsforms/{self.unowned.item.id}/check',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_resolve(self): def test_resolve(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
@ -385,7 +386,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/resolve', f'/api/rsforms/{schema.item.id}/resolve',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам') self.assertEqual(response.data['output'], 'редким синим слонам')
self.assertEqual(len(response.data['refs']), 2) self.assertEqual(len(response.data['refs']), 2)
@ -411,7 +412,7 @@ class TestRSFormViewset(APITestCase):
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file} data = {'file': file}
response = self.client.post('/api/rsforms/import-trs', data=data, format='multipart') response = self.client.post('/api/rsforms/import-trs', data=data, format='multipart')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertTrue(response.data['title'] != '') self.assertTrue(response.data['title'] != '')
@ -419,7 +420,7 @@ class TestRSFormViewset(APITestCase):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs') response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file: with ZipFile(stream, 'r') as zipped_file:
@ -432,7 +433,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/cst-create', f'/api/rsforms/{self.unowned.item.id}/cst-create',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
item = self.owned.item item = self.owned.item
Constituenta.objects.create( Constituenta.objects.create(
@ -451,7 +452,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create', f'/api/rsforms/{item.id}/cst-create',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
@ -467,7 +468,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create', f'/api/rsforms/{item.id}/cst-create',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['new_cst']['alias'], data['alias']) self.assertEqual(response.data['new_cst']['alias'], data['alias'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
@ -503,27 +504,27 @@ class TestRSFormViewset(APITestCase):
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'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
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, status.HTTP_400_BAD_REQUEST)
data = {'id': cst1.pk, 'alias': cst1.alias, 'cst_type': 'term'} data = {'id': cst1.pk, 'alias': cst1.alias, 'cst_type': 'term'}
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, status.HTTP_400_BAD_REQUEST)
data = {'id': cst1.pk, 'alias': cst3.alias} data = {'id': cst1.pk, 'alias': cst3.alias}
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, status.HTTP_400_BAD_REQUEST)
data = {'alias': 'D2', 'cst_type': 'term', 'id': cst1.pk} data = {'alias': 'D2', 'cst_type': 'term', 'id': cst1.pk}
item = self.owned.item item = self.owned.item
@ -540,7 +541,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-rename', f'/api/rsforms/{item.id}/cst-rename',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
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()
@ -578,27 +579,27 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/cst-substitute', f'/api/rsforms/{self.unowned.item.id}/cst-substitute',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute', f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True} data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute', f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True} data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute', f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
d1 = Constituenta.objects.create( d1 = Constituenta.objects.create(
alias='D1', alias='D1',
@ -612,7 +613,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.owned.item.id}/cst-substitute', f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
d1.refresh_from_db() d1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
@ -634,7 +635,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create', f'/api/rsforms/{item.id}/cst-create',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
self.assertEqual(response.data['new_cst']['cst_type'], 'basic') self.assertEqual(response.data['new_cst']['cst_type'], 'basic')
self.assertEqual(response.data['new_cst']['convention'], '1') self.assertEqual(response.data['new_cst']['convention'], '1')
@ -651,7 +652,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/cst-delete-multiple', f'/api/rsforms/{schema.item.id}/cst-delete-multiple',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2) x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2)
@ -662,7 +663,7 @@ class TestRSFormViewset(APITestCase):
) )
x2.refresh_from_db() x2.refresh_from_db()
schema.item.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 202) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(len(response.data['items']), 1) self.assertEqual(len(response.data['items']), 1)
self.assertEqual(schema.constituents().count(), 1) self.assertEqual(schema.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2') self.assertEqual(x2.alias, 'X2')
@ -674,7 +675,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/cst-delete-multiple', f'/api/rsforms/{schema.item.id}/cst-delete-multiple',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_move_constituenta(self): def test_move_constituenta(self):
item = self.owned.item item = self.owned.item
@ -683,7 +684,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-moveto', f'/api/rsforms/{item.id}/cst-moveto',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
@ -694,7 +695,7 @@ class TestRSFormViewset(APITestCase):
) )
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['id'], item.id) self.assertEqual(response.data['id'], item.id)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
@ -705,12 +706,12 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-moveto', f'/api/rsforms/{item.id}/cst-moveto',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_reset_aliases(self): def test_reset_aliases(self):
item = self.owned.item item = self.owned.item
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases') response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['id'], item.id) self.assertEqual(response.data['id'], item.id)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1)
@ -720,7 +721,7 @@ class TestRSFormViewset(APITestCase):
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
d11.refresh_from_db() d11.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x2.alias, 'X1') self.assertEqual(x2.alias, 'X1')
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
@ -729,7 +730,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(d11.alias, 'D1') self.assertEqual(d11.alias, 'D1')
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases') response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_load_trs(self): def test_load_trs(self):
schema = self.owned schema = self.owned
@ -744,7 +745,7 @@ class TestRSFormViewset(APITestCase):
data=data, format='multipart' data=data, format='multipart'
) )
schema.item.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(schema.item.title, 'Test11') self.assertEqual(schema.item.title, 'Test11')
self.assertEqual(len(response.data['items']), 25) self.assertEqual(len(response.data['items']), 25)
self.assertEqual(schema.constituents().count(), 25) self.assertEqual(schema.constituents().count(), 25)
@ -769,7 +770,7 @@ class TestRSFormViewset(APITestCase):
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['items'][0]['alias'], x1.alias) self.assertEqual(response.data['items'][0]['alias'], x1.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x1.term_raw) self.assertEqual(response.data['items'][0]['term_raw'], x1.term_raw)
@ -778,6 +779,55 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['items'][1]['term_resolved'], d1.term_resolved) self.assertEqual(response.data['items'][1]['term_resolved'], d1.term_resolved)
class TestVersionViews(APITestCase):
''' Testing versioning endpoints. '''
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.unowned = RSForm.create(title='Test2', alias='T2')
self.x1 = Constituenta.objects.create(
schema=self.owned.item,
alias='X1',
cst_type='basic',
convention='testStart',
order=1
)
def test_create_version(self):
invalid_data = {'description': 'test'}
data = {'version': '1.0.0', 'description': 'test'}
invalid_id = 1338
response = self.client.post(
f'/api/rsforms/{invalid_id}/versions/create',
data=data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.client.post(
f'/api/rsforms/{self.unowned.item.id}/versions/create',
data=data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.post(
f'/api/rsforms/{self.owned.item.id}/versions/create',
data=invalid_data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post(
f'/api/rsforms/{self.owned.item.id}/versions/create',
data=data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue('version' in response.data)
self.assertTrue('schema' in response.data)
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
class TestRSLanguageViews(APITestCase): class TestRSLanguageViews(APITestCase):
def setUp(self): def setUp(self):
self.factory = APIRequestFactory() self.factory = APIRequestFactory()
@ -793,7 +843,7 @@ class TestRSLanguageViews(APITestCase):
'/api/rsforms/create-detailed', '/api/rsforms/create-detailed',
data=data, format='multipart' data=data, format='multipart'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], 'Test123') self.assertEqual(response.data['title'], 'Test123')
self.assertEqual(response.data['alias'], 'ks1') self.assertEqual(response.data['alias'], 'ks1')
@ -805,7 +855,7 @@ class TestRSLanguageViews(APITestCase):
'/api/rsforms/create-detailed', '/api/rsforms/create-detailed',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], 'Test123') self.assertEqual(response.data['title'], 'Test123')
self.assertEqual(response.data['alias'], 'ks1') self.assertEqual(response.data['alias'], 'ks1')
@ -818,7 +868,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = convert_to_ascii(request) response = convert_to_ascii(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['result'], r'1 \eq 1') self.assertEqual(response.data['result'], r'1 \eq 1')
def test_convert_to_ascii_missing_data(self): def test_convert_to_ascii_missing_data(self):
@ -828,7 +878,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = convert_to_ascii(request) response = convert_to_ascii(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
def test_convert_to_math(self): def test_convert_to_math(self):
@ -838,7 +888,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = convert_to_math(request) response = convert_to_math(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['result'], r'1=1') self.assertEqual(response.data['result'], r'1=1')
def test_convert_to_math_missing_data(self): def test_convert_to_math_missing_data(self):
@ -848,7 +898,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = convert_to_math(request) response = convert_to_math(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
def test_parse_expression(self): def test_parse_expression(self):
@ -858,7 +908,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = parse_expression(request) response = parse_expression(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], Syntax.MATH) self.assertEqual(response.data['syntax'], Syntax.MATH)
self.assertEqual(response.data['astText'], '[=[1][1]]') self.assertEqual(response.data['astText'], '[=[1][1]]')
@ -870,7 +920,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = parse_expression(request) response = parse_expression(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
@ -889,7 +939,7 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = parse_text(request) response = parse_text(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc') self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc')
def test_inflect(self): def test_inflect(self):
@ -899,7 +949,7 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = inflect(request) response = inflect(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['result'], 'синим слонам') self.assertEqual(response.data['result'], 'синим слонам')
def test_generate_lexeme(self): def test_generate_lexeme(self):
@ -909,6 +959,6 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json' data=data, format='json'
) )
response = generate_lexeme(request) response = generate_lexeme(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['items']), 12) self.assertEqual(len(response.data['items']), 12)
self.assertEqual(response.data['items'][0]['text'], 'синий слон') self.assertEqual(response.data['items'][0]['text'], 'синий слон')

View File

@ -13,6 +13,7 @@ urlpatterns = [
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),
path('rsforms/<int:pk_item>/versions/create', views.create_version),
path('rslang/parse-expression', views.parse_expression), path('rslang/parse-expression', views.parse_expression),
path('rslang/to-ascii', views.convert_to_ascii), path('rslang/to-ascii', views.convert_to_ascii),

View File

@ -1,14 +1,14 @@
''' REST API: RSForms for conceptual schemas. ''' ''' REST API: RSForms for conceptual schemas. '''
import json import json
from typing import cast from typing import cast, Union
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q from django.db.models import Q
from rest_framework import views, viewsets, filters, generics, permissions from rest_framework import views, viewsets, filters, generics, permissions
from rest_framework.decorators import action from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import api_view from rest_framework.request import Request
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status as c from rest_framework import status as c
@ -27,14 +27,14 @@ class LibraryActiveView(generics.ListAPIView):
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request.user.is_anonymous:
if not user.is_anonymous: return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
else:
user = cast(m.User, self.request.user)
# pylint: disable=unsupported-binary-operation # pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter( return m.LibraryItem.objects.filter(
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user) Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
).distinct().order_by('-time_update') ).distinct().order_by('-time_update')
else:
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
@extend_schema(tags=['Library']) @extend_schema(tags=['Library'])
@ -86,14 +86,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update']: if self.action in ['update', 'destroy', 'partial_update']:
permission_classes = [utils.ObjectOwnerOrAdmin] permission_list = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']: elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
permission_classes = [permissions.IsAuthenticated] permission_list = [permissions.IsAuthenticated]
elif self.action in ['claim']: elif self.action in ['claim']:
permission_classes = [utils.IsClaimable] permission_list = [utils.IsClaimable]
else: else:
permission_classes = [permissions.AllowAny] permission_list = [permissions.AllowAny]
return [permission() for permission in permission_classes] return [permission() for permission in permission_list]
def _get_item(self) -> m.LibraryItem: def _get_item(self) -> m.LibraryItem:
return cast(m.LibraryItem, self.get_object()) return cast(m.LibraryItem, self.get_object())
@ -109,7 +109,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['post'], url_path='clone') @action(detail=True, methods=['post'], url_path='clone')
def clone(self, request, pk): def clone(self, request: Request, pk):
''' Endpoint: Create deep copy of library item. ''' ''' Endpoint: Create deep copy of library item. '''
serializer = s.LibraryItemSerializer(data=request.data) serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -141,13 +141,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def claim(self, request, pk=None): def claim(self, request: Request, pk=None):
''' Endpoint: Claim ownership of LibraryItem. ''' ''' Endpoint: Claim ownership of LibraryItem. '''
item = self._get_item() item = self._get_item()
if item.owner == self.request.user: if item.owner == self.request.user:
return Response(status=304) return Response(status=c.HTTP_304_NOT_MODIFIED)
else: else:
item.owner = self.request.user item.owner = cast(m.User, self.request.user)
item.save() item.save()
m.Subscription.subscribe(user=item.owner, item=item) m.Subscription.subscribe(user=item.owner, item=item)
return Response( return Response(
@ -162,10 +162,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
responses={c.HTTP_204_NO_CONTENT: None} responses={c.HTTP_204_NO_CONTENT: None}
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def subscribe(self, request, pk): def subscribe(self, request: Request, pk):
''' Endpoint: Subscribe current user to item. ''' ''' Endpoint: Subscribe current user to item. '''
item = self._get_item() item = self._get_item()
m.Subscription.subscribe(user=self.request.user, item=item) m.Subscription.subscribe(user=cast(m.User, self.request.user), item=item)
return Response(status=c.HTTP_204_NO_CONTENT) return Response(status=c.HTTP_204_NO_CONTENT)
@extend_schema( @extend_schema(
@ -175,10 +175,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
responses={c.HTTP_204_NO_CONTENT: None}, responses={c.HTTP_204_NO_CONTENT: None},
) )
@action(detail=True, methods=['delete']) @action(detail=True, methods=['delete'])
def unsubscribe(self, request, pk): def unsubscribe(self, request: Request, pk):
''' Endpoint: Unsubscribe current user from item. ''' ''' Endpoint: Unsubscribe current user from item. '''
item = self._get_item() item = self._get_item()
m.Subscription.unsubscribe(user=self.request.user, item=item) m.Subscription.unsubscribe(user=cast(m.User, self.request.user), item=item)
return Response(status=c.HTTP_204_NO_CONTENT) return Response(status=c.HTTP_204_NO_CONTENT)
@ -190,7 +190,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.RSForm: def _get_schema(self) -> m.RSForm:
return m.RSForm(self.get_object()) # type: ignore return m.RSForm(cast(m.LibraryItem, self.get_object()))
def get_permissions(self): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
@ -208,7 +208,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_201_CREATED: s.NewCstResponse} responses={c.HTTP_201_CREATED: s.NewCstResponse}
) )
@action(detail=True, methods=['post'], url_path='cst-create') @action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request, pk): def cst_create(self, request: Request, pk):
''' Create new constituenta. ''' ''' Create new constituenta. '''
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
@ -237,7 +237,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-rename') @action(detail=True, methods=['patch'], url_path='cst-rename')
def cst_rename(self, request, pk): def cst_rename(self, request: Request, pk):
''' Rename constituenta possibly changing type. ''' ''' Rename constituenta possibly changing type. '''
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema}) serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
@ -264,7 +264,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-substitute') @action(detail=True, methods=['patch'], url_path='cst-substitute')
def cst_substitute(self, request, pk): def cst_substitute(self, request: Request, pk):
''' Substitute occurrences of constituenta with another one. ''' ''' Substitute occurrences of constituenta with another one. '''
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': schema}) serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': schema})
@ -287,7 +287,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_202_ACCEPTED: s.RSFormParseSerializer} responses={c.HTTP_202_ACCEPTED: s.RSFormParseSerializer}
) )
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple') @action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
def cst_delete_multiple(self, request, pk): def cst_delete_multiple(self, request: Request, pk):
''' Endpoint: Delete multiple constituents. ''' ''' Endpoint: Delete multiple constituents. '''
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstListSerializer( serializer = s.CstListSerializer(
@ -309,7 +309,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={c.HTTP_200_OK: s.RSFormParseSerializer}
) )
@action(detail=True, methods=['patch'], url_path='cst-moveto') @action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk): def cst_moveto(self, request: Request, pk):
''' Endpoint: Move multiple constituents. ''' ''' Endpoint: Move multiple constituents. '''
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstMoveSerializer( serializer = s.CstMoveSerializer(
@ -334,7 +334,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={c.HTTP_200_OK: s.RSFormParseSerializer}
) )
@action(detail=True, methods=['patch'], url_path='reset-aliases') @action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request, pk): def reset_aliases(self, request: Request, pk):
''' Endpoint: Recreate all aliases based on order. ''' ''' Endpoint: Recreate all aliases based on order. '''
schema = self._get_schema() schema = self._get_schema()
schema.reset_aliases() schema.reset_aliases()
@ -350,12 +350,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={c.HTTP_200_OK: s.RSFormParseSerializer}
) )
@action(detail=True, methods=['patch'], url_path='load-trs') @action(detail=True, methods=['patch'], url_path='load-trs')
def load_trs(self, request, pk): def load_trs(self, request: Request, pk):
''' Endpoint: Load data from file and replace current schema. ''' ''' Endpoint: Load data from file and replace current schema. '''
serializer = s.RSFormUploadSerializer(data=request.data) input_serializer = s.RSFormUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True) input_serializer.is_valid(raise_exception=True)
schema = self._get_schema() schema = self._get_schema()
load_metadata = serializer.validated_data['load_metadata'] load_metadata = input_serializer.validated_data['load_metadata']
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
data['id'] = schema.item.pk data['id'] = schema.item.pk
@ -364,10 +364,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'load_meta': load_metadata} context={'load_meta': load_metadata}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() result = serializer.save()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(result.item).data
) )
@extend_schema( @extend_schema(
@ -377,7 +377,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormSerializer} responses={c.HTTP_200_OK: s.RSFormSerializer}
) )
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def contents(self, request, pk): def contents(self, request: Request, pk):
''' Endpoint: View schema db contents (including constituents). ''' ''' Endpoint: View schema db contents (including constituents). '''
schema = s.RSFormSerializer(self.get_object()) schema = s.RSFormSerializer(self.get_object())
return Response( return Response(
@ -392,10 +392,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={c.HTTP_200_OK: s.RSFormParseSerializer}
) )
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def details(self, request, pk): def details(self, request: Request, pk):
''' Endpoint: Detailed schema view including statuses and parse. ''' ''' Endpoint: Detailed schema view including statuses and parse. '''
schema = self._get_schema() serializer = s.RSFormParseSerializer(cast(m.LibraryItem, self.get_object()))
serializer = s.RSFormParseSerializer(schema.item)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=serializer.data data=serializer.data
@ -408,7 +407,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.ExpressionParseSerializer}, responses={c.HTTP_200_OK: s.ExpressionParseSerializer},
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def check(self, request, pk): def check(self, request: Request, pk):
''' Endpoint: Check RSLang expression against schema context. ''' ''' Endpoint: Check RSLang expression against schema context. '''
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -427,7 +426,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.ResolverSerializer} responses={c.HTTP_200_OK: s.ResolverSerializer}
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def resolve(self, request, pk): def resolve(self, request: Request, pk):
''' Endpoint: Resolve references in text against schema terms context. ''' ''' Endpoint: Resolve references in text against schema terms context. '''
serializer = s.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -446,7 +445,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={(c.HTTP_200_OK, 'application/zip'): bytes} responses={(c.HTTP_200_OK, 'application/zip'): bytes}
) )
@action(detail=True, methods=['get'], url_path='export-trs') @action(detail=True, methods=['get'], url_path='export-trs')
def export_trs(self, request, pk): def export_trs(self, request: Request, pk):
''' Endpoint: Download Exteor compatible file. ''' ''' Endpoint: Download Exteor compatible file. '''
schema = s.RSFormTRSSerializer(self._get_schema()).data schema = s.RSFormTRSSerializer(self._get_schema()).data
trs = utils.write_trs(schema) trs = utils.write_trs(schema)
@ -465,6 +464,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
class TrsImportView(views.APIView): class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. ''' ''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = s.FileSerializer serializer_class = s.FileSerializer
permission_classes = [permissions.IsAuthenticated]
@extend_schema( @extend_schema(
summary='import TRS file into RSForm', summary='import TRS file into RSForm',
@ -472,11 +472,9 @@ class TrsImportView(views.APIView):
request=s.FileSerializer, request=s.FileSerializer,
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
) )
def post(self, request): def post(self, request: Request):
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
owner = self.request.user owner = cast(m.User, self.request.user)
if owner.is_anonymous:
owner = None
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer( serializer = s.RSFormTRSSerializer(
data=data, data=data,
@ -498,11 +496,9 @@ class TrsImportView(views.APIView):
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
) )
@api_view(['POST']) @api_view(['POST'])
def create_rsform(request): def create_rsform(request: Request):
''' Endpoint: Create RSForm from user input and/or trs file. ''' ''' Endpoint: Create RSForm from user input and/or trs file. '''
owner = request.user owner = cast(m.User, request.user) if not request.user.is_anonymous else None
if owner.is_anonymous:
owner = None
if 'file' not in request.FILES: if 'file' not in request.FILES:
serializer = s.LibraryItemSerializer(data=request.data) serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -517,16 +513,17 @@ def create_rsform(request):
else: else:
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True) serializer_rsform.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema.item) result = s.LibraryItemSerializer(schema.item)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=result.data data=result.data
) )
def _prepare_rsform_data(data: dict, request, owner: m.User):
def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None]):
data['owner'] = owner data['owner'] = owner
if 'title' in request.data and request.data['title'] != '': if 'title' in request.data and request.data['title'] != '':
data['title'] = request.data['title'] data['title'] = request.data['title']
@ -548,6 +545,45 @@ def _prepare_rsform_data(data: dict, request, owner: m.User):
data['is_canonical'] = is_canonical data['is_canonical'] = is_canonical
@extend_schema(
summary='save version for RSForm copying current content',
tags=['Versions'],
request=s.VersionCreateSerializer,
responses={
c.HTTP_201_CREATED: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def create_version(request: Request, pk_item: int):
''' Endpoint: Create new version for RSForm copying current content. '''
try:
item = m.LibraryItem.objects.get(pk=pk_item)
except m.LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
creator = request.user
if not creator.is_staff and creator != item.owner:
return Response(status=c.HTTP_403_FORBIDDEN)
version_input = s.VersionCreateSerializer(data=request.data)
version_input.is_valid(raise_exception=True)
data = s.RSFormSerializer(item).to_versioned_data()
result = m.RSForm(item).create_version(
version=version_input.validated_data['version'],
description=version_input.validated_data['description'],
data=data
)
return Response(
status=c.HTTP_201_CREATED,
data={
'version': result.pk,
'schema': s.RSFormParseSerializer(item).data
}
)
@extend_schema( @extend_schema(
summary='RS expression into Syntax Tree', summary='RS expression into Syntax Tree',
tags=['FormalLanguage'], tags=['FormalLanguage'],
@ -556,7 +592,7 @@ def _prepare_rsform_data(data: dict, request, owner: m.User):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def parse_expression(request): def parse_expression(request: Request):
''' Endpoint: Parse RS expression. ''' ''' Endpoint: Parse RS expression. '''
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -576,7 +612,7 @@ def parse_expression(request):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def convert_to_ascii(request): def convert_to_ascii(request: Request):
''' Endpoint: Convert expression to ASCII syntax. ''' ''' Endpoint: Convert expression to ASCII syntax. '''
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -596,7 +632,7 @@ def convert_to_ascii(request):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def convert_to_math(request): def convert_to_math(request: Request):
''' Endpoint: Convert expression to MATH syntax. ''' ''' Endpoint: Convert expression to MATH syntax. '''
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -615,7 +651,7 @@ def convert_to_math(request):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def inflect(request): def inflect(request: Request):
''' Endpoint: Generate wordform with set grammemes. ''' ''' Endpoint: Generate wordform with set grammemes. '''
serializer = s.WordFormSerializer(data=request.data) serializer = s.WordFormSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -636,7 +672,7 @@ def inflect(request):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def generate_lexeme(request): def generate_lexeme(request: Request):
''' Endpoint: Generate complete set of wordforms for lexeme. ''' ''' Endpoint: Generate complete set of wordforms for lexeme. '''
serializer = s.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -656,7 +692,7 @@ def generate_lexeme(request):
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def parse_text(request): def parse_text(request: Request):
''' Endpoint: Get likely vocabulary parse. ''' ''' Endpoint: Get likely vocabulary parse. '''
serializer = s.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)

View File

@ -394,8 +394,7 @@ class PhraseParser:
def _filtered_parse(text: str): def _filtered_parse(text: str):
capital = Capitalization.from_text(text) capital = Capitalization.from_text(text)
score_filter = PhraseParser._filter_score(morpho.parse(text)) score_filter = PhraseParser._filter_score(morpho.parse(text))
for form in PhraseParser._filter_capital(score_filter, capital): yield from PhraseParser._filter_capital(score_filter, capital)
yield form
@staticmethod @staticmethod
def _filter_score(generator): def _filter_score(generator):
@ -412,8 +411,7 @@ class PhraseParser:
continue continue
yield form yield form
else: else:
for form in generator: yield from generator
yield form
@staticmethod @staticmethod
def _parse_word(text: str, require_index: int = INDEX_NONE, def _parse_word(text: str, require_index: int = INDEX_NONE,

View File

@ -10,6 +10,8 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
''' '''
import sys
import logging
import os import os
from pathlib import Path from pathlib import Path
@ -229,6 +231,10 @@ LOGGING = {
}, },
'root': { 'root': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO')
}, },
} }
if len(sys.argv) > 1 and sys.argv[1] == 'test':
logging.disable(logging.CRITICAL)

View File

@ -86,6 +86,16 @@ export enum LibraryItemType {
OPERATIONS_SCHEMA = 'oss' OPERATIONS_SCHEMA = 'oss'
} }
/**
* Represents library item version information.
*/
export interface IVersionInfo {
id: number;
version: string;
description: string;
time_create: string;
}
/** /**
* Represents library item common data typical for all item types. * Represents library item common data typical for all item types.
*/ */
@ -107,6 +117,8 @@ export interface ILibraryItem {
*/ */
export interface ILibraryItemEx extends ILibraryItem { export interface ILibraryItemEx extends ILibraryItem {
subscribers: number[]; subscribers: number[];
version?: number;
versions: IVersionInfo[];
} }
/** /**