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
PYTHONDEVMODE=1
PYTHONTRACEMALLOC=1
DJANGO_LOG_LEVEL=DEBUG

View File

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

View File

@ -34,3 +34,4 @@ DB_PASSWORD=78ACF6C4F3
DEBUG=0
PYTHONDEVMODE=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.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
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

@ -125,9 +125,13 @@ class LibraryItem(Model):
return f'/api/library/{self.pk}'
def subscribers(self) -> list[User]:
''' Get all subscribers for this item . '''
''' Get all subscribers for this item. '''
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
def save(self, *args, **kwargs):
subscribe = not self.pk and self.owner
@ -151,6 +155,40 @@ class LibraryTemplate(Model):
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):
''' User subscription to library item. '''
user: ForeignKey = ForeignKey(
@ -511,6 +549,16 @@ class RSForm:
cst.definition_resolved = resolved
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
def _reset_order(self):
order = 1

View File

@ -8,7 +8,7 @@ import pyconcept
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
from .utils import fix_old_references
from .models import Constituenta, LibraryItem, RSForm
from .models import Constituenta, LibraryItem, RSForm, Version
from . import messages as msg
_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):
''' Serializer: LibraryItem detailed data. '''
subscribers = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField()
class Meta:
''' serializer metadata. '''
@ -141,6 +159,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
def get_subscribers(self, instance: LibraryItem) -> list[int]:
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):
''' Serializer: Constituenta data. '''
@ -398,7 +419,7 @@ class RSFormSerializer(serializers.ModelSerializer):
model = LibraryItem
fields = '__all__'
def to_representation(self, instance: LibraryItem):
def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data
schema = RSForm(instance)
result['items'] = []
@ -406,6 +427,25 @@ class RSFormSerializer(serializers.ModelSerializer):
result['items'].append(ConstituentaSerializer(cst).data)
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):
''' Serializer: Constituenta data including parse. '''

View File

@ -4,6 +4,7 @@ import io
from zipfile import ZipFile
from rest_framework.test import APITestCase, APIRequestFactory, APIClient
from rest_framework.exceptions import ErrorDetail
from rest_framework import status
from cctext import ReferenceType, split_grams
@ -63,7 +64,7 @@ class TestConstituentaAPI(APITestCase):
def test_retrieve(self):
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['convention'], self.cst1.convention)
@ -73,21 +74,21 @@ class TestConstituentaAPI(APITestCase):
f'/api/constituents/{self.cst2.id}',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.logout()
response = self.client.patch(
f'/api/constituents/{self.cst1.id}',
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)
response = self.client.patch(
f'/api/constituents/{self.cst1.id}',
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.assertEqual(response.data['convention'], 'tt')
self.assertEqual(self.cst1.convention, 'tt')
@ -97,7 +98,7 @@ class TestConstituentaAPI(APITestCase):
data=data,
format='json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_update_resolved_no_refs(self):
data = {
@ -105,7 +106,7 @@ class TestConstituentaAPI(APITestCase):
'definition_raw': 'New def'
}
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.assertEqual(response.data['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}',
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.assertEqual(self.cst3.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}',
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'], self.cst1.alias)
self.assertEqual(response.data['order'], self.cst1.order)
@ -169,12 +170,12 @@ class TestLibraryViewset(APITestCase):
self.client.logout()
data = {'title': 'Title'}
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):
data = {'title': 'Title'}
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['owner'], self.user.id)
@ -184,7 +185,7 @@ class TestLibraryViewset(APITestCase):
f'/api/library/{self.owned.id}',
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['alias'], self.owned.alias)
@ -194,37 +195,37 @@ class TestLibraryViewset(APITestCase):
f'/api/library/{self.unowned.id}',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_destroy(self):
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):
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.save()
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):
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.save()
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')
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(self.user in self.unowned.subscribers())
self.unowned.is_common = True
self.unowned.save()
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.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):
self.client.logout()
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):
self.client.logout()
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.assertFalse(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned))
def test_retrieve_owned(self):
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.assertFalse(_response_contains(response, self.unowned))
self.assertTrue(_response_contains(response, self.owned))
def test_retrieve_subscribed(self):
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))
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.owned)
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.assertEqual(len(response.data), 3)
def test_subscriptions(self):
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())
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())
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())
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())
def test_retrieve_templates(self):
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.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.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(_response_contains(response, self.common))
self.assertTrue(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned))
@ -312,13 +313,13 @@ class TestRSFormViewset(APITestCase):
title='Test3'
)
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.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item))
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, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item))
@ -327,7 +328,7 @@ class TestRSFormViewset(APITestCase):
schema = RSForm.create(title='Title1')
schema.insert_last(alias='X1', insert_type=CstType.BASE)
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):
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')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test')
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.id)
@ -362,7 +363,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/check',
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['syntax'], Syntax.MATH)
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
@ -373,7 +374,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/check',
data=data, format='json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_resolve(self):
schema = RSForm.create(title='Test')
@ -385,7 +386,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/resolve',
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['output'], 'редким синим слонам')
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:
data = {'file': file}
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.assertTrue(response.data['title'] != '')
@ -419,7 +420,7 @@ class TestRSFormViewset(APITestCase):
schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE)
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')
with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file:
@ -432,7 +433,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/cst-create',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
item = self.owned.item
Constituenta.objects.create(
@ -451,7 +452,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create',
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')
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 3)
@ -467,7 +468,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create',
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'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 3)
@ -503,27 +504,27 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
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'}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
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}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
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}
item = self.owned.item
@ -540,7 +541,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-rename',
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']['cst_type'], 'term')
d1.refresh_from_db()
@ -578,27 +579,27 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.unowned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
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}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
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}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
d1 = Constituenta.objects.create(
alias='D1',
@ -612,7 +613,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, status.HTTP_200_OK)
d1.refresh_from_db()
x2.refresh_from_db()
@ -634,7 +635,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-create',
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']['cst_type'], 'basic')
self.assertEqual(response.data['new_cst']['convention'], '1')
@ -651,7 +652,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/cst-delete-multiple',
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)
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()
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(schema.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2')
@ -674,7 +675,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{schema.item.id}/cst-delete-multiple',
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):
item = self.owned.item
@ -683,7 +684,7 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-moveto',
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)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
@ -694,7 +695,7 @@ class TestRSFormViewset(APITestCase):
)
x1.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(x1.order, 2)
self.assertEqual(x2.order, 1)
@ -705,12 +706,12 @@ class TestRSFormViewset(APITestCase):
f'/api/rsforms/{item.id}/cst-moveto',
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):
item = self.owned.item
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)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1)
@ -720,7 +721,7 @@ class TestRSFormViewset(APITestCase):
x1.refresh_from_db()
x2.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.alias, 'X1')
self.assertEqual(x1.order, 2)
@ -729,7 +730,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(d11.alias, 'D1')
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):
schema = self.owned
@ -744,7 +745,7 @@ class TestRSFormViewset(APITestCase):
data=data, format='multipart'
)
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(len(response.data['items']), 25)
self.assertEqual(schema.constituents().count(), 25)
@ -769,7 +770,7 @@ class TestRSFormViewset(APITestCase):
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['items'][0]['alias'], x1.alias)
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)
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):
def setUp(self):
self.factory = APIRequestFactory()
@ -793,7 +843,7 @@ class TestRSLanguageViews(APITestCase):
'/api/rsforms/create-detailed',
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['title'], 'Test123')
self.assertEqual(response.data['alias'], 'ks1')
@ -805,7 +855,7 @@ class TestRSLanguageViews(APITestCase):
'/api/rsforms/create-detailed',
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['title'], 'Test123')
self.assertEqual(response.data['alias'], 'ks1')
@ -818,7 +868,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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')
def test_convert_to_ascii_missing_data(self):
@ -828,7 +878,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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)
def test_convert_to_math(self):
@ -838,7 +888,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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')
def test_convert_to_math_missing_data(self):
@ -848,7 +898,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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)
def test_parse_expression(self):
@ -858,7 +908,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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['syntax'], Syntax.MATH)
self.assertEqual(response.data['astText'], '[=[1][1]]')
@ -870,7 +920,7 @@ class TestRSLanguageViews(APITestCase):
data=data, format='json'
)
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)
@ -889,7 +939,7 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json'
)
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')
def test_inflect(self):
@ -899,7 +949,7 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json'
)
response = inflect(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['result'], 'синим слонам')
def test_generate_lexeme(self):
@ -909,6 +959,6 @@ class TestNaturalLanguageViews(APITestCase):
data=data, format='json'
)
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(response.data['items'][0]['text'], 'синий слон')

View File

@ -13,6 +13,7 @@ urlpatterns = [
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs', views.TrsImportView.as_view()),
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/to-ascii', views.convert_to_ascii),

View File

@ -1,14 +1,14 @@
''' REST API: RSForms for conceptual schemas. '''
import json
from typing import cast
from typing import cast, Union
from django.db import transaction
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
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.decorators import api_view
from rest_framework.request import Request
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status as c
@ -27,14 +27,14 @@ class LibraryActiveView(generics.ListAPIView):
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
user = self.request.user
if not user.is_anonymous:
if self.request.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
return m.LibraryItem.objects.filter(
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
).distinct().order_by('-time_update')
else:
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
@extend_schema(tags=['Library'])
@ -86,14 +86,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update']:
permission_classes = [utils.ObjectOwnerOrAdmin]
permission_list = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
permission_classes = [permissions.IsAuthenticated]
permission_list = [permissions.IsAuthenticated]
elif self.action in ['claim']:
permission_classes = [utils.IsClaimable]
permission_list = [utils.IsClaimable]
else:
permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes]
permission_list = [permissions.AllowAny]
return [permission() for permission in permission_list]
def _get_item(self) -> m.LibraryItem:
return cast(m.LibraryItem, self.get_object())
@ -109,7 +109,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
)
@transaction.atomic
@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. '''
serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -141,13 +141,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
)
@transaction.atomic
@action(detail=True, methods=['post'])
def claim(self, request, pk=None):
def claim(self, request: Request, pk=None):
''' Endpoint: Claim ownership of LibraryItem. '''
item = self._get_item()
if item.owner == self.request.user:
return Response(status=304)
return Response(status=c.HTTP_304_NOT_MODIFIED)
else:
item.owner = self.request.user
item.owner = cast(m.User, self.request.user)
item.save()
m.Subscription.subscribe(user=item.owner, item=item)
return Response(
@ -162,10 +162,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
responses={c.HTTP_204_NO_CONTENT: None}
)
@action(detail=True, methods=['post'])
def subscribe(self, request, pk):
def subscribe(self, request: Request, pk):
''' Endpoint: Subscribe current user to 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)
@extend_schema(
@ -175,10 +175,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
responses={c.HTTP_204_NO_CONTENT: None},
)
@action(detail=True, methods=['delete'])
def unsubscribe(self, request, pk):
def unsubscribe(self, request: Request, pk):
''' Endpoint: Unsubscribe current user from 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)
@ -190,7 +190,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer_class = s.LibraryItemSerializer
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):
''' Determine permission class. '''
@ -208,7 +208,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_201_CREATED: s.NewCstResponse}
)
@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. '''
schema = self._get_schema()
serializer = s.CstCreateSerializer(data=request.data)
@ -237,7 +237,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@transaction.atomic
@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. '''
schema = self._get_schema()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
@ -264,7 +264,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@transaction.atomic
@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. '''
schema = self._get_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}
)
@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. '''
schema = self._get_schema()
serializer = s.CstListSerializer(
@ -309,7 +309,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@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. '''
schema = self._get_schema()
serializer = s.CstMoveSerializer(
@ -334,7 +334,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@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. '''
schema = self._get_schema()
schema.reset_aliases()
@ -350,12 +350,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@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. '''
serializer = s.RSFormUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
input_serializer = s.RSFormUploadSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
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['id'] = schema.item.pk
@ -364,10 +364,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'load_meta': load_metadata}
)
serializer.is_valid(raise_exception=True)
schema = serializer.save()
result = serializer.save()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(result.item).data
)
@extend_schema(
@ -377,7 +377,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormSerializer}
)
@action(detail=True, methods=['get'])
def contents(self, request, pk):
def contents(self, request: Request, pk):
''' Endpoint: View schema db contents (including constituents). '''
schema = s.RSFormSerializer(self.get_object())
return Response(
@ -392,10 +392,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@action(detail=True, methods=['get'])
def details(self, request, pk):
def details(self, request: Request, pk):
''' Endpoint: Detailed schema view including statuses and parse. '''
schema = self._get_schema()
serializer = s.RSFormParseSerializer(schema.item)
serializer = s.RSFormParseSerializer(cast(m.LibraryItem, self.get_object()))
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -408,7 +407,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
responses={c.HTTP_200_OK: s.ExpressionParseSerializer},
)
@action(detail=True, methods=['post'])
def check(self, request, pk):
def check(self, request: Request, pk):
''' Endpoint: Check RSLang expression against schema context. '''
serializer = s.ExpressionSerializer(data=request.data)
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}
)
@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. '''
serializer = s.TextSerializer(data=request.data)
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}
)
@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. '''
schema = s.RSFormTRSSerializer(self._get_schema()).data
trs = utils.write_trs(schema)
@ -465,6 +464,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = s.FileSerializer
permission_classes = [permissions.IsAuthenticated]
@extend_schema(
summary='import TRS file into RSForm',
@ -472,11 +472,9 @@ class TrsImportView(views.APIView):
request=s.FileSerializer,
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
)
def post(self, request):
def post(self, request: Request):
data = utils.read_trs(request.FILES['file'].file)
owner = self.request.user
if owner.is_anonymous:
owner = None
owner = cast(m.User, self.request.user)
_prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(
data=data,
@ -498,11 +496,9 @@ class TrsImportView(views.APIView):
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
)
@api_view(['POST'])
def create_rsform(request):
def create_rsform(request: Request):
''' Endpoint: Create RSForm from user input and/or trs file. '''
owner = request.user
if owner.is_anonymous:
owner = None
owner = cast(m.User, request.user) if not request.user.is_anonymous else None
if 'file' not in request.FILES:
serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -517,16 +513,17 @@ def create_rsform(request):
else:
data = utils.read_trs(request.FILES['file'].file)
_prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True)
schema = serializer.save()
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True)
schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema.item)
return Response(
status=c.HTTP_201_CREATED,
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
if 'title' in request.data and 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
@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(
summary='RS expression into Syntax Tree',
tags=['FormalLanguage'],
@ -556,7 +592,7 @@ def _prepare_rsform_data(data: dict, request, owner: m.User):
auth=None
)
@api_view(['POST'])
def parse_expression(request):
def parse_expression(request: Request):
''' Endpoint: Parse RS expression. '''
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -576,7 +612,7 @@ def parse_expression(request):
auth=None
)
@api_view(['POST'])
def convert_to_ascii(request):
def convert_to_ascii(request: Request):
''' Endpoint: Convert expression to ASCII syntax. '''
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -596,7 +632,7 @@ def convert_to_ascii(request):
auth=None
)
@api_view(['POST'])
def convert_to_math(request):
def convert_to_math(request: Request):
''' Endpoint: Convert expression to MATH syntax. '''
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -615,7 +651,7 @@ def convert_to_math(request):
auth=None
)
@api_view(['POST'])
def inflect(request):
def inflect(request: Request):
''' Endpoint: Generate wordform with set grammemes. '''
serializer = s.WordFormSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -636,7 +672,7 @@ def inflect(request):
auth=None
)
@api_view(['POST'])
def generate_lexeme(request):
def generate_lexeme(request: Request):
''' Endpoint: Generate complete set of wordforms for lexeme. '''
serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -656,7 +692,7 @@ def generate_lexeme(request):
auth=None
)
@api_view(['POST'])
def parse_text(request):
def parse_text(request: Request):
''' Endpoint: Get likely vocabulary parse. '''
serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

View File

@ -394,8 +394,7 @@ class PhraseParser:
def _filtered_parse(text: str):
capital = Capitalization.from_text(text)
score_filter = PhraseParser._filter_score(morpho.parse(text))
for form in PhraseParser._filter_capital(score_filter, capital):
yield form
yield from PhraseParser._filter_capital(score_filter, capital)
@staticmethod
def _filter_score(generator):
@ -412,8 +411,7 @@ class PhraseParser:
continue
yield form
else:
for form in generator:
yield form
yield from generator
@staticmethod
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/
'''
import sys
import logging
import os
from pathlib import Path
@ -229,6 +231,10 @@ LOGGING = {
},
'root': {
'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'
}
/**
* 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.
*/
@ -107,6 +117,8 @@ export interface ILibraryItem {
*/
export interface ILibraryItemEx extends ILibraryItem {
subscribers: number[];
version?: number;
versions: IVersionInfo[];
}
/**