mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implementing versioning: backend createVersion
This commit is contained in:
parent
e0deefd0cb
commit
ca4882575e
|
@ -34,3 +34,4 @@ DB_PASSWORD=78ACF6C4F3
|
|||
DEBUG=1
|
||||
PYTHONDEVMODE=1
|
||||
PYTHONTRACEMALLOC=1
|
||||
DJANGO_LOG_LEVEL=DEBUG
|
|
@ -34,3 +34,4 @@ DB_PORT=5432
|
|||
DEBUG=0
|
||||
PYTHONDEVMODE=0
|
||||
PYTHONTRACEMALLOC=0
|
||||
DJANGO_LOG_LEVEL=DEBUG
|
|
@ -34,3 +34,4 @@ DB_PASSWORD=78ACF6C4F3
|
|||
DEBUG=0
|
||||
PYTHONDEVMODE=0
|
||||
PYTHONTRACEMALLOC=0
|
||||
DJANGO_LOG_LEVEL=DEBUG
|
|
@ -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)
|
||||
|
|
30
rsconcept/backend/apps/rsform/migrations/0004_version.py
Normal file
30
rsconcept/backend/apps/rsform/migrations/0004_version.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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. '''
|
||||
|
|
|
@ -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'], 'синий слон')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user