R: restructure backend

Warning! This will reset database migrations. Data should be imported manually
This commit is contained in:
Ivan 2024-07-25 19:12:31 +03:00
parent 95caab2919
commit 90dcf7a8eb
88 changed files with 4639 additions and 4785 deletions

View File

@ -0,0 +1,62 @@
''' Admin view: Library. '''
from django.contrib import admin
from . import models
class LibraryItemAdmin(admin.ModelAdmin):
''' Admin model: LibraryItem. '''
date_hierarchy = 'time_update'
list_display = [
'alias', 'title', 'owner',
'visible', 'read_only', 'access_policy', 'location',
'time_update'
]
list_filter = ['visible', 'read_only', 'access_policy', 'location', 'time_update']
search_fields = ['alias', 'title', 'location']
class LibraryTemplateAdmin(admin.ModelAdmin):
''' Admin model: LibraryTemplate. '''
list_display = ['id', 'alias']
list_select_related = ['lib_source']
def alias(self, template: models.LibraryTemplate):
if template.lib_source:
return template.lib_source.alias
else:
return 'N/A'
class SubscriptionAdmin(admin.ModelAdmin):
''' Admin model: Subscriptions. '''
list_display = ['id', 'item', 'user']
search_fields = [
'item__title', 'item__alias',
'user__username', 'user__first_name', 'user__last_name'
]
class EditorAdmin(admin.ModelAdmin):
''' Admin model: Editors. '''
list_display = ['id', 'item', 'editor']
search_fields = [
'item__title', 'item__alias',
'editor__username', 'editor__first_name', 'editor__last_name'
]
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.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
admin.site.register(models.Subscription, SubscriptionAdmin)
admin.site.register(models.Version, VersionAdmin)
admin.site.register(models.Editor, EditorAdmin)

View File

@ -0,0 +1,8 @@
''' Application: Operation Schema. '''
from django.apps import AppConfig
class RsformConfig(AppConfig):
''' Application config. '''
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.library'

View File

@ -0,0 +1,92 @@
# Generated by Django 5.0.7 on 2024-07-25 16:06
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LibraryItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_type', models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operation Schema')], default='rsform', max_length=50, verbose_name='Тип')),
('title', models.TextField(verbose_name='Название')),
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
('comment', models.TextField(blank=True, verbose_name='Комментарий')),
('visible', models.BooleanField(default=True, verbose_name='Отображаемая')),
('read_only', models.BooleanField(default=False, verbose_name='Запретить редактирование')),
('access_policy', models.CharField(choices=[('public', 'Public'), ('protected', 'Protected'), ('private', 'Private')], default='public', max_length=500, verbose_name='Политика доступа')),
('location', models.TextField(default='/U', max_length=500, verbose_name='Расположение')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')),
],
options={
'verbose_name': 'Схема',
'verbose_name_plural': 'Схемы',
},
),
migrations.CreateModel(
name='LibraryTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lib_source', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Источник')),
],
options={
'verbose_name': 'Шаблон',
'verbose_name_plural': 'Шаблоны',
},
),
migrations.CreateModel(
name='Editor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
('editor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Редактор')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Схема')),
],
options={
'verbose_name': 'Редактор',
'verbose_name_plural': 'Редакторы',
'unique_together': {('item', 'editor')},
},
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Элемент')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Подписка',
'verbose_name_plural': 'Подписки',
'unique_together': {('user', 'item')},
},
),
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='library.libraryitem', verbose_name='Схема')),
],
options={
'verbose_name': 'Версия',
'verbose_name_plural': 'Версии',
'unique_together': {('item', 'version')},
},
),
]

View File

@ -14,7 +14,7 @@ class Editor(Model):
''' Editor list. ''' ''' Editor list. '''
item: ForeignKey = ForeignKey( item: ForeignKey = ForeignKey(
verbose_name='Схема', verbose_name='Схема',
to='rsform.LibraryItem', to='library.LibraryItem',
on_delete=CASCADE on_delete=CASCADE
) )
editor: ForeignKey = ForeignKey( editor: ForeignKey = ForeignKey(

View File

@ -54,7 +54,8 @@ class LibraryItem(Model):
item_type: CharField = CharField( item_type: CharField = CharField(
verbose_name='Тип', verbose_name='Тип',
max_length=50, max_length=50,
choices=LibraryItemType.choices choices=LibraryItemType.choices,
default=LibraryItemType.RSFORM
) )
owner: ForeignKey = ForeignKey( owner: ForeignKey = ForeignKey(
verbose_name='Владелец', verbose_name='Владелец',

View File

@ -6,7 +6,7 @@ class LibraryTemplate(Model):
''' Template for library items and constituents. ''' ''' Template for library items and constituents. '''
lib_source: ForeignKey = ForeignKey( lib_source: ForeignKey = ForeignKey(
verbose_name='Источник', verbose_name='Источник',
to='rsform.RSForm', to='library.LibraryItem',
on_delete=CASCADE, on_delete=CASCADE,
null=True null=True
) )

View File

@ -18,7 +18,7 @@ class Subscription(Model):
) )
item: ForeignKey = ForeignKey( item: ForeignKey = ForeignKey(
verbose_name='Элемент', verbose_name='Элемент',
to='rsform.LibraryItem', to='library.LibraryItem',
on_delete=CASCADE on_delete=CASCADE
) )

View File

@ -14,7 +14,7 @@ class Version(Model):
''' Library item version archive. ''' ''' Library item version archive. '''
item: ForeignKey = ForeignKey( item: ForeignKey = ForeignKey(
verbose_name='Схема', verbose_name='Схема',
to='rsform.LibraryItem', to='library.LibraryItem',
on_delete=CASCADE on_delete=CASCADE
) )
version = CharField( version = CharField(

View File

@ -0,0 +1,7 @@
''' Django: Models. '''
from .Editor import Editor
from .LibraryItem import AccessPolicy, LibraryItem, LibraryItemType, LocationHead, validate_location
from .LibraryTemplate import LibraryTemplate
from .Subscription import Subscription
from .Version import Version

View File

@ -0,0 +1,14 @@
''' REST API: Serializers. '''
from .basics import AccessPolicySerializer, LocationSerializer
from .data_access import (
LibraryItemBaseSerializer,
LibraryItemCloneSerializer,
LibraryItemDetailsSerializer,
LibraryItemSerializer,
UsersListSerializer,
UserTargetSerializer,
VersionCreateSerializer,
VersionSerializer
)
from .responses import NewVersionResponse

View File

@ -0,0 +1,32 @@
''' Basic serializers that do not interact with database. '''
from rest_framework import serializers
from shared import messages as msg
from ..models import AccessPolicy, validate_location
class LocationSerializer(serializers.Serializer):
''' Serializer: Item location. '''
location = serializers.CharField(max_length=500)
def validate(self, attrs):
attrs = super().validate(attrs)
if not validate_location(attrs['location']):
raise serializers.ValidationError({
'location': msg.invalidLocation()
})
return attrs
class AccessPolicySerializer(serializers.Serializer):
''' Serializer: Constituenta renaming. '''
access_policy = serializers.CharField()
def validate(self, attrs):
attrs = super().validate(attrs)
if not attrs['access_policy'] in AccessPolicy.values:
raise serializers.ValidationError({
'access_policy': msg.invalidEnum(attrs['access_policy'])
})
return attrs

View File

@ -0,0 +1,94 @@
''' Serializers for persistent data manipulation. '''
from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.rsform.models import Constituenta
from ..models import LibraryItem, Version
class LibraryItemBaseSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry full access. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('id',)
class LibraryItemSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry limited access. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy')
class LibraryItemCloneSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem cloning. '''
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
class Meta:
''' serializer metadata. '''
model = LibraryItem
exclude = ['id', 'item_type', 'owner']
class VersionSerializer(serializers.ModelSerializer):
''' Serializer: Version data. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'id', 'version', 'item', 'description', 'time_create'
read_only_fields = ('id', 'item', 'time_create')
class VersionInnerSerializer(serializers.ModelSerializer):
''' Serializer: Version data for list of versions. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'id', 'version', 'description', 'time_create'
read_only_fields = ('id', 'item', '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()
editors = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField()
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('owner', 'id', 'item_type')
def get_subscribers(self, instance: LibraryItem) -> list[int]:
return [item.pk for item in instance.subscribers()]
def get_editors(self, instance: LibraryItem) -> list[int]:
return [item.pk for item in instance.editors()]
def get_versions(self, instance: LibraryItem) -> list:
return [VersionInnerSerializer(item).data for item in instance.versions()]
class UserTargetSerializer(serializers.Serializer):
''' Serializer: Target single User. '''
user = PKField(many=False, queryset=User.objects.all())
class UsersListSerializer(serializers.Serializer):
''' Serializer: List of Users. '''
users = PKField(many=True, queryset=User.objects.all())

View File

@ -0,0 +1,8 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers
class NewVersionResponse(serializers.Serializer):
''' Serializer: Create version response. '''
version = serializers.IntegerField()
schema = serializers.JSONField()

View File

@ -0,0 +1,3 @@
''' Tests. '''
from .s_models import *
from .s_views import *

View File

@ -0,0 +1,4 @@
''' Tests for Django Models. '''
from .t_Editor import *
from .t_LibraryItem import *
from .t_Subscription import *

View File

@ -1,7 +1,8 @@
''' Testing models: Editor. ''' ''' Testing models: Editor. '''
from django.test import TestCase from django.test import TestCase
from apps.rsform.models import Editor, LibraryItemType, RSForm, User from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.users.models import User
class TestEditor(TestCase): class TestEditor(TestCase):
@ -10,7 +11,8 @@ class TestEditor(TestCase):
def setUp(self): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
self.item = RSForm.objects.create( self.item = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test', title='Test',
alias='КС1', alias='КС1',
owner=self.user1 owner=self.user1

View File

@ -1,15 +1,15 @@
''' Testing models: LibraryItem. ''' ''' Testing models: LibraryItem. '''
from django.test import TestCase from django.test import TestCase
from apps.rsform.models import ( from apps.library.models import (
AccessPolicy, AccessPolicy,
LibraryItem, LibraryItem,
LibraryItemType, LibraryItemType,
LocationHead, LocationHead,
Subscription, Subscription,
User,
validate_location validate_location
) )
from apps.users.models import User
class TestLibraryItem(TestCase): class TestLibraryItem(TestCase):

View File

@ -1,7 +1,8 @@
''' Testing models: Subscription. ''' ''' Testing models: Subscription. '''
from django.test import TestCase from django.test import TestCase
from apps.rsform.models import LibraryItem, LibraryItemType, Subscription, User from apps.library.models import LibraryItem, LibraryItemType, Subscription
from apps.users.models import User
class TestSubscription(TestCase): class TestSubscription(TestCase):

View File

@ -0,0 +1,3 @@
''' Tests for REST API. '''
from .t_library import *
from .t_versions import *

View File

@ -1,16 +1,16 @@
''' Testing API: Library. ''' ''' Testing API: Library. '''
from rest_framework import status from rest_framework import status
from apps.rsform.models import ( from apps.library.models import (
AccessPolicy, AccessPolicy,
Editor, Editor,
LibraryItem, LibraryItem,
LibraryItemType, LibraryItemType,
LibraryTemplate, LibraryTemplate,
LocationHead, LocationHead,
RSForm,
Subscription Subscription
) )
from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains from shared.testing_utils import response_contains
@ -20,16 +20,16 @@ class TestLibraryViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = RSForm.objects.create( self.owned = LibraryItem.objects.create(
title='Test', title='Test',
alias='T1', alias='T1',
owner=self.user owner=self.user
) )
self.unowned = RSForm.objects.create( self.unowned = LibraryItem.objects.create(
title='Test2', title='Test2',
alias='T2' alias='T2'
) )
self.common = RSForm.objects.create( self.common = LibraryItem.objects.create(
title='Test3', title='Test3',
alias='T3', alias='T3',
location=LocationHead.COMMON location=LocationHead.COMMON
@ -44,12 +44,16 @@ class TestLibraryViewset(EndpointTester):
'title': 'Title', 'title': 'Title',
'alias': 'alias', 'alias': 'alias',
} }
self.executeBadData(data=data) response = self.executeCreated(data=data)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], LibraryItemType.RSFORM)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], data['alias'])
data = { data = {
'item_type': LibraryItemType.OPERATION_SCHEMA, 'item_type': LibraryItemType.OPERATION_SCHEMA,
'title': 'Title', 'title': 'Title2',
'alias': 'alias', 'alias': 'alias2',
'access_policy': AccessPolicy.PROTECTED, 'access_policy': AccessPolicy.PROTECTED,
'visible': False, 'visible': False,
'read_only': True 'read_only': True
@ -359,12 +363,13 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/clone', method='post') @decl_endpoint('/api/library/{item}/clone', method='post')
def test_clone_rsform(self): def test_clone_rsform(self):
x12 = self.owned.insert_new( schema = RSForm(self.owned)
x12 = schema.insert_new(
alias='X12', alias='X12',
term_raw='человек', term_raw='человек',
term_resolved='человек' term_resolved='человек'
) )
d2 = self.owned.insert_new( d2 = schema.insert_new(
alias='D2', alias='D2',
term_raw='@{X12|plur}', term_raw='@{X12|plur}',
term_resolved='люди' term_resolved='люди'

View File

@ -15,51 +15,72 @@ class TestVersionViews(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.unowned = RSForm.objects.create(title='Test2', alias='T2') self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.x1 = self.owned.insert_new( self.x1 = self.owned.insert_new(
alias='X1', alias='X1',
convention='testStart' convention='testStart'
) )
@decl_endpoint('/api/rsforms/{schema}/versions/create', method='post') @decl_endpoint('/api/library/{schema}/create-version', method='post')
def test_create_version(self): def test_create_version(self):
invalid_data = {'description': 'test'} invalid_data = {'description': 'test'}
invalid_id = 1338 invalid_id = 1338
data = {'version': '1.0.0', 'description': 'test'} data = {'version': '1.0.0', 'description': 'test'}
self.executeNotFound(data=data, schema=invalid_id) self.executeNotFound(data=data, schema=invalid_id)
self.executeForbidden(data=data, schema=self.unowned.pk) self.executeForbidden(data=data, schema=self.unowned_id)
self.executeBadData(data=invalid_data, schema=self.owned.pk) self.executeBadData(data=invalid_data, schema=self.owned_id)
response = self.executeCreated(data=data, schema=self.owned.pk) response = self.executeCreated(data=data, schema=self.owned_id)
self.assertTrue('version' in response.data) self.assertTrue('version' in response.data)
self.assertTrue('schema' in response.data) self.assertTrue('schema' in response.data)
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']]) self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
@decl_endpoint('/api/rsforms/{schema}/versions/{version}', method='get') @decl_endpoint('/api/library/{schema}/versions/{version}', method='get')
def test_retrieve_version(self): def test_retrieve_version(self):
version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
invalid_id = version_id + 1337 invalid_id = version_id + 1337
self.executeNotFound(schema=invalid_id, version=invalid_id) self.executeNotFound(schema=invalid_id, version=invalid_id)
self.executeNotFound(schema=self.owned.pk, version=invalid_id) self.executeNotFound(schema=self.owned_id, version=invalid_id)
self.executeNotFound(schema=invalid_id, version=version_id) self.executeNotFound(schema=invalid_id, version=version_id)
self.executeNotFound(schema=self.unowned.pk, version=version_id) self.executeNotFound(schema=self.unowned_id, version=version_id)
self.owned.alias = 'NewName' self.owned.model.alias = 'NewName'
self.owned.save() self.owned.save()
self.x1.alias = 'X33' self.x1.alias = 'X33'
self.x1.save() self.x1.save()
response = self.executeOK(schema=self.owned.pk, version=version_id) response = self.executeOK(schema=self.owned_id, version=version_id)
self.assertNotEqual(response.data['alias'], self.owned.alias) self.assertNotEqual(response.data['alias'], self.owned.model.alias)
self.assertNotEqual(response.data['items'][0]['alias'], self.x1.alias) self.assertNotEqual(response.data['items'][0]['alias'], self.x1.alias)
self.assertEqual(response.data['version'], version_id) self.assertEqual(response.data['version'], version_id)
@decl_endpoint('/api/library/{schema}/versions/{version}', method='get')
def test_retrieve_version_details(self):
a1 = Constituenta.objects.create(
schema=self.owned.model,
alias='A1',
cst_type='axiom',
definition_formal='X1=X1',
order=2
)
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
a1.definition_formal = 'X1=X2'
a1.save()
response = self.executeOK(schema=self.owned_id, version=version_id)
loaded_a1 = response.data['items'][1]
self.assertEqual(loaded_a1['definition_formal'], 'X1=X1')
self.assertEqual(loaded_a1['parse']['status'], 'verified')
@decl_endpoint('/api/versions/{version}', method='get') @decl_endpoint('/api/versions/{version}', method='get')
def test_access_version(self): def test_access_version(self):
data = {'version': '1.0.0', 'description': 'test'} data = {'version': '1.0.0', 'description': 'test'}
@ -73,7 +94,7 @@ class TestVersionViews(EndpointTester):
response = self.executeOK() response = self.executeOK()
self.assertEqual(response.data['version'], data['version']) self.assertEqual(response.data['version'], data['version'])
self.assertEqual(response.data['description'], data['description']) self.assertEqual(response.data['description'], data['description'])
self.assertEqual(response.data['item'], self.owned.pk) self.assertEqual(response.data['item'], self.owned_id)
data = {'version': '1.2.0', 'description': 'test1'} data = {'version': '1.2.0', 'description': 'test1'}
self.method = 'patch' self.method = 'patch'
@ -95,25 +116,6 @@ class TestVersionViews(EndpointTester):
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@decl_endpoint('/api/rsforms/{schema}/versions/{version}', method='get')
def test_retrieve_version_details(self):
a1 = Constituenta.objects.create(
schema=self.owned,
alias='A1',
cst_type='axiom',
definition_formal='X1=X1',
order=2
)
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
a1.definition_formal = 'X1=X2'
a1.save()
response = self.executeOK(schema=self.owned.pk, version=version_id)
loaded_a1 = response.data['items'][1]
self.assertEqual(loaded_a1['definition_formal'], 'X1=X1')
self.assertEqual(loaded_a1['parse']['status'], 'verified')
@decl_endpoint('/api/versions/{version}/export-file', method='get') @decl_endpoint('/api/versions/{version}/export-file', method='get')
def test_export_version(self): def test_export_version(self):
invalid_id = 1338 invalid_id = 1338
@ -123,7 +125,7 @@ class TestVersionViews(EndpointTester):
response = self.executeOK(version=version_id) response = self.executeOK(version=version_id)
self.assertEqual( self.assertEqual(
response.headers['Content-Disposition'], response.headers['Content-Disposition'],
f'attachment; filename={self.owned.alias}.trs' f'attachment; filename={self.owned.model.alias}.trs'
) )
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file: with ZipFile(stream, 'r') as zipped_file:
@ -165,7 +167,7 @@ class TestVersionViews(EndpointTester):
def _create_version(self, data) -> int: def _create_version(self, data) -> int:
response = self.client.post( response = self.client.post(
f'/api/rsforms/{self.owned.pk}/versions/create', f'/api/library/{self.owned_id}/create-version',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)

View File

@ -0,0 +1,21 @@
''' Routing: Operation Schema. '''
from django.urls import include, path
from rest_framework import routers
from . import views
library_router = routers.SimpleRouter(trailing_slash=False)
library_router.register('library', views.LibraryViewSet, 'Library')
library_router.register('versions', views.VersionViewset, 'Version')
urlpatterns = [
path('library/active', views.LibraryActiveView.as_view()),
path('library/all', views.LibraryAdminView.as_view()),
path('library/templates', views.LibraryTemplatesView.as_view(), name='templates'),
path('library/<int:pk_item>/create-version', views.create_version),
path('library/<int:pk_item>/versions/<int:pk_version>', views.retrieve_version),
path('versions/<int:pk>/export-file', views.export_file),
path('', include(library_router.urls)),
]

View File

@ -0,0 +1,3 @@
''' REST API: Endpoint processors. '''
from .library import LibraryActiveView, LibraryAdminView, LibraryTemplatesView, LibraryViewSet
from .versions import VersionViewset, create_version, export_file, retrieve_version

View File

@ -13,6 +13,9 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User
from shared import permissions from shared import permissions
from .. import models as m from .. import models as m
@ -73,7 +76,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
tags=['Library'], tags=['Library'],
request=s.LibraryItemCloneSerializer, request=s.LibraryItemCloneSerializer,
responses={ responses={
c.HTTP_201_CREATED: s.RSFormParseSerializer, c.HTTP_201_CREATED: RSFormParseSerializer,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None, c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
@ -88,8 +91,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
if item.item_type != m.LibraryItemType.RSFORM: if item.item_type != m.LibraryItemType.RSFORM:
return Response(status=c.HTTP_400_BAD_REQUEST) return Response(status=c.HTTP_400_BAD_REQUEST)
schema = m.RSForm.objects.get(pk=item.pk) clone = deepcopy(item)
clone = deepcopy(schema)
clone.pk = None clone.pk = None
clone.owner = self.request.user clone.owner = self.request.user
clone.title = serializer.validated_data['title'] clone.title = serializer.validated_data['title']
@ -103,14 +105,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
clone.save() clone.save()
need_filter = 'items' in request.data need_filter = 'items' in request.data
for cst in schema.constituents(): for cst in RSForm(item).constituents():
if not need_filter or cst.pk in request.data['items']: if not need_filter or cst.pk in request.data['items']:
cst.pk = None cst.pk = None
cst.schema = clone cst.schema = clone
cst.save() cst.save()
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=s.RSFormParseSerializer(clone).data data=RSFormParseSerializer(clone).data
) )
@extend_schema( @extend_schema(
@ -127,7 +129,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
def subscribe(self, request: Request, pk): def subscribe(self, request: Request, pk):
''' Endpoint: Subscribe current user to item. ''' ''' Endpoint: Subscribe current user to item. '''
item = self._get_item() item = self._get_item()
m.Subscription.subscribe(user=cast(m.User, self.request.user), item=item) m.Subscription.subscribe(user=cast(User, self.request.user), item=item)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -144,7 +146,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
def unsubscribe(self, request: Request, pk): def unsubscribe(self, request: Request, pk):
''' Endpoint: Unsubscribe current user from item. ''' ''' Endpoint: Unsubscribe current user from item. '''
item = self._get_item() item = self._get_item()
m.Subscription.unsubscribe(user=cast(m.User, self.request.user), item=item) m.Subscription.unsubscribe(user=cast(User, self.request.user), item=item)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -184,7 +186,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
item = self._get_item() item = self._get_item()
serializer = s.AccessPolicySerializer(data=request.data) serializer = s.AccessPolicySerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=serializer.validated_data['access_policy']) new_policy = serializer.validated_data['access_policy']
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=new_policy)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -286,7 +289,7 @@ class LibraryActiveView(generics.ListAPIView):
.filter(is_public) \ .filter(is_public) \
.filter(common_location).order_by('-time_update') .filter(common_location).order_by('-time_update')
else: else:
user = cast(m.User, self.request.user) user = cast(User, self.request.user)
# pylint: disable=unsupported-binary-operation # pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter( return m.LibraryItem.objects.filter(
(is_public & common_location) | (is_public & common_location) |

View File

@ -10,11 +10,13 @@ from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from shared import permissions from apps.rsform import utils
from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer, RSFormSerializer, RSFormTRSSerializer
from shared import permissions, utility
from .. import models as m from .. import models as m
from .. import serializers as s from .. import serializers as s
from .. import utils
@extend_schema(tags=['Version']) @extend_schema(tags=['Version'])
@ -32,7 +34,7 @@ class VersionViewset(
summary='restore version data into current item', summary='restore version data into current item',
request=None, request=None,
responses={ responses={
c.HTTP_200_OK: s.RSFormParseSerializer, c.HTTP_200_OK: RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None, c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
@ -42,81 +44,10 @@ class VersionViewset(
''' Restore version data into current item. ''' ''' Restore version data into current item. '''
version = cast(m.Version, self.get_object()) version = cast(m.Version, self.get_object())
item = cast(m.LibraryItem, version.item) item = cast(m.LibraryItem, version.item)
schema = m.RSForm.objects.get(pk=item.pk) RSFormSerializer(item).restore_from_version(version.data)
s.RSFormSerializer(schema).restore_from_version(version.data)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data data=RSFormParseSerializer(item).data
)
@extend_schema(
summary='save version for RSForm copying current content',
tags=['Version'],
request=s.VersionCreateSerializer,
responses={
c.HTTP_201_CREATED: s.NewVersionResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@api_view(['POST'])
@permission_classes([permissions.GlobalUser])
def create_version(request: Request, pk_item: int):
''' Endpoint: Create new version for RSForm copying current content. '''
try:
item = m.RSForm.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 = 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='retrieve versioned data for RSForm',
tags=['Version'],
request=None,
responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_404_NOT_FOUND: None
}
)
@api_view(['GET'])
def retrieve_version(request: Request, pk_item: int, pk_version: int):
''' Endpoint: Retrieve version for RSForm. '''
try:
item = m.RSForm.objects.get(pk=pk_item)
except m.RSForm.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
try:
version = m.Version.objects.get(pk=pk_version)
except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
if version.item != item:
return Response(status=c.HTTP_404_NOT_FOUND)
data = s.RSFormParseSerializer(item).from_versioned_data(version.pk, version.data)
return Response(
status=c.HTTP_200_OK,
data=data
) )
@ -136,10 +67,79 @@ def export_file(request: Request, pk: int):
version = m.Version.objects.get(pk=pk) version = m.Version.objects.get(pk=pk)
except m.Version.DoesNotExist: except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_404_NOT_FOUND)
schema = m.RSForm.objects.get(pk=version.item.pk) data = RSFormTRSSerializer(version.item).from_versioned_data(version.data)
data = s.RSFormTRSSerializer(schema).from_versioned_data(version.data) file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(data['alias']) filename = utils.filename_for_schema(data['alias'])
response = HttpResponse(file, content_type='application/zip') response = HttpResponse(file, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}' response['Content-Disposition'] = f'attachment; filename={filename}'
return response return response
@extend_schema(
summary='save version for RSForm copying current content',
tags=['Version'],
request=s.VersionCreateSerializer,
responses={
c.HTTP_201_CREATED: s.NewVersionResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@api_view(['POST'])
@permission_classes([permissions.GlobalUser])
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 = RSFormSerializer(item).to_versioned_data()
result = 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': RSFormParseSerializer(item).data
}
)
@extend_schema(
summary='retrieve versioned data for RSForm',
tags=['Version'],
request=None,
responses={
c.HTTP_200_OK: RSFormParseSerializer,
c.HTTP_404_NOT_FOUND: None
}
)
@api_view(['GET'])
def retrieve_version(request: Request, pk_item: int, pk_version: int):
''' Endpoint: Retrieve version for RSForm. '''
try:
item = m.LibraryItem.objects.get(pk=pk_item)
except m.LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
try:
version = m.Version.objects.get(pk=pk_version)
except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
if version.item != item:
return Response(status=c.HTTP_404_NOT_FOUND)
data = RSFormParseSerializer(item).from_versioned_data(version.pk, version.data)
return Response(
status=c.HTTP_200_OK,
data=data
)

View File

@ -27,4 +27,4 @@ class SynthesisSubstitutionAdmin(admin.ModelAdmin):
admin.site.register(models.Operation, OperationAdmin) admin.site.register(models.Operation, OperationAdmin)
admin.site.register(models.Argument, ArgumentAdmin) admin.site.register(models.Argument, ArgumentAdmin)
admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin) admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)

View File

@ -2,7 +2,7 @@
from django.apps import AppConfig from django.apps import AppConfig
class RsformConfig(AppConfig): class OssConfig(AppConfig):
''' Application config. ''' ''' Application config. '''
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.oss' name = 'apps.oss'

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-07-17 09:51 # Generated by Django 5.0.7 on 2024-07-25 16:06
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -9,7 +9,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('rsform', '0008_alter_libraryitem_item_type'), ('library', '0001_initial'),
('rsform', '0001_initial'),
] ]
operations = [ operations = [
@ -17,17 +18,15 @@ class Migration(migrations.Migration):
name='Operation', name='Operation',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('operation_type', models.CharField(choices=[ ('operation_type', models.CharField(choices=[('input', 'Input'), ('synthesis', 'Synthesis')], default='input', max_length=10, verbose_name='Тип')),
('input', 'Input'), ('synthesis', 'Synthesis')], default='input', max_length=10, verbose_name='Тип')), ('sync_text', models.BooleanField(default=True, verbose_name='Синхронизация')),
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')), ('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
('title', models.TextField(blank=True, verbose_name='Название')), ('title', models.TextField(blank=True, verbose_name='Название')),
('comment', models.TextField(blank=True, verbose_name='Комментарий')), ('comment', models.TextField(blank=True, verbose_name='Комментарий')),
('position_x', models.FloatField(default=0, verbose_name='Положение по горизонтали')), ('position_x', models.FloatField(default=0, verbose_name='Положение по горизонтали')),
('position_y', models.FloatField(default=0, verbose_name='Положение по вертикали')), ('position_y', models.FloatField(default=0, verbose_name='Положение по вертикали')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, ('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='library.libraryitem', verbose_name='Схема синтеза')),
related_name='items', to='rsform.libraryitem', verbose_name='Схема синтеза')), ('result', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='producer', to='library.libraryitem', verbose_name='Связанная КС')),
('result', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='producer', to='rsform.libraryitem', verbose_name='Связанная КС')),
], ],
options={ options={
'verbose_name': 'Операция', 'verbose_name': 'Операция',
@ -35,16 +34,13 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SynthesisSubstitution', name='Substitution',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer_term', models.BooleanField(default=False, verbose_name='Перенос термина')), ('transfer_term', models.BooleanField(default=False, verbose_name='Перенос термина')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, ('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oss.operation', verbose_name='Операция')),
to='oss.operation', verbose_name='Операция')), ('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_original', to='rsform.constituenta', verbose_name='Удаляемая конституента')),
('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, ('substitution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_substitute', to='rsform.constituenta', verbose_name='Замещающая конституента')),
related_name='as_original', to='rsform.constituenta', verbose_name='Удаляемая конституента')),
('substitution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='as_substitute', to='rsform.constituenta', verbose_name='Замещающая конституента')),
], ],
options={ options={
'verbose_name': 'Отождествление синтеза', 'verbose_name': 'Отождествление синтеза',
@ -55,10 +51,8 @@ class Migration(migrations.Migration):
name='Argument', name='Argument',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, ('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='oss.operation', verbose_name='Аргумент')),
related_name='descendants', to='oss.operation', verbose_name='Аргумент')), ('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arguments', to='oss.operation', verbose_name='Операция')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='arguments', to='oss.operation', verbose_name='Операция')),
], ],
options={ options={
'verbose_name': 'Аргумент', 'verbose_name': 'Аргумент',

View File

@ -1,31 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-22 13:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0001_initial'),
('rsform', '0008_alter_libraryitem_item_type'),
]
operations = [
migrations.CreateModel(
name='OperationSchema',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('rsform.libraryitem',),
),
migrations.AlterField(
model_name='operation',
name='oss',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='oss.operationschema', verbose_name='Схема синтеза'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-24 18:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0002_operationschema_alter_operation_oss'),
('rsform', '0009_rsform_alter_constituenta_schema_and_more'),
]
operations = [
migrations.AddField(
model_name='operation',
name='sync_text',
field=models.BooleanField(default=True, verbose_name='Синхронизация'),
),
migrations.AlterField(
model_name='operation',
name='result',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='producer', to='rsform.rsform', verbose_name='Связанная КС'),
),
]

View File

@ -0,0 +1,32 @@
''' Models: Synthesis Inheritance. '''
from django.db.models import CASCADE, ForeignKey, Model
class Inheritance(Model):
''' Inheritance links parent and child constituents in synthesis operation.'''
operation: ForeignKey = ForeignKey(
verbose_name='Операция',
to='oss.Operation',
on_delete=CASCADE
)
parent: ForeignKey = ForeignKey(
verbose_name='Исходная конституента',
to='rsform.Constituenta',
on_delete=CASCADE,
related_name='as_parent'
)
child: ForeignKey = ForeignKey(
verbose_name='Наследованная конституента',
to='rsform.Constituenta',
on_delete=CASCADE,
related_name='as_child'
)
class Meta:
''' Model metadata. '''
verbose_name = 'Наследование синтеза'
verbose_name_plural = 'Отношение наследования конституент'
def __str__(self) -> str:
return f'{self.parent} -> {self.child}'

View File

@ -22,7 +22,7 @@ class Operation(Model):
''' Operational schema Unit.''' ''' Operational schema Unit.'''
oss: ForeignKey = ForeignKey( oss: ForeignKey = ForeignKey(
verbose_name='Схема синтеза', verbose_name='Схема синтеза',
to='oss.OperationSchema', to='library.LibraryItem',
on_delete=CASCADE, on_delete=CASCADE,
related_name='items' related_name='items'
) )
@ -34,7 +34,7 @@ class Operation(Model):
) )
result: ForeignKey = ForeignKey( result: ForeignKey = ForeignKey(
verbose_name='Связанная КС', verbose_name='Связанная КС',
to='rsform.RSForm', to='library.LibraryItem',
null=True, null=True,
on_delete=SET_NULL, on_delete=SET_NULL,
related_name='producer' related_name='producer'

View File

@ -3,47 +3,53 @@ from typing import Optional
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import Manager, QuerySet from django.db.models import QuerySet
from apps.rsform.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from shared import messages as msg from shared import messages as msg
from .Argument import Argument from .Argument import Argument
from .Operation import Operation from .Operation import Operation
from .SynthesisSubstitution import SynthesisSubstitution from .Substitution import Substitution
class OperationSchema(LibraryItem): class OperationSchema:
''' Operations schema API. ''' ''' Operations schema API. '''
class Meta: def __init__(self, model: LibraryItem):
''' Model metadata. ''' self.model = model
proxy = True
class InternalManager(Manager): @staticmethod
''' Object manager. ''' def create(**kwargs) -> 'OperationSchema':
''' Create LibraryItem via OperationSchema. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
return OperationSchema(model)
def get_queryset(self) -> QuerySet: @staticmethod
return super().get_queryset().filter(item_type=LibraryItemType.OPERATION_SCHEMA) def from_id(pk: int) -> 'OperationSchema':
''' Get LibraryItem by pk. '''
model = LibraryItem.objects.get(pk=pk)
return OperationSchema(model)
def create(self, **kwargs): def save(self, *args, **kwargs):
kwargs.update({'item_type': LibraryItemType.OPERATION_SCHEMA}) ''' Save wrapper. '''
return super().create(**kwargs) self.model.save(*args, **kwargs)
# Legit overriding object manager def refresh_from_db(self):
objects = InternalManager() # type: ignore[misc] ''' Model wrapper. '''
self.model.refresh_from_db()
def operations(self) -> QuerySet[Operation]: def operations(self) -> QuerySet[Operation]:
''' Get QuerySet containing all operations of current OSS. ''' ''' Get QuerySet containing all operations of current OSS. '''
return Operation.objects.filter(oss=self) return Operation.objects.filter(oss=self.model)
def arguments(self) -> QuerySet[Argument]: def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. ''' ''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self) return Argument.objects.filter(operation__oss=self.model)
def substitutions(self) -> QuerySet[SynthesisSubstitution]: def substitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. ''' ''' Operation substitutions. '''
return SynthesisSubstitution.objects.filter(operation__oss=self) return Substitution.objects.filter(operation__oss=self.model)
def update_positions(self, data: list[dict]): def update_positions(self, data: list[dict]):
''' Update positions. ''' ''' Update positions. '''
@ -60,7 +66,7 @@ class OperationSchema(LibraryItem):
''' Insert new operation. ''' ''' Insert new operation. '''
if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists(): if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists():
raise ValidationError(msg.aliasTaken(kwargs['alias'])) raise ValidationError(msg.aliasTaken(kwargs['alias']))
result = Operation.objects.create(oss=self, **kwargs) result = Operation.objects.create(oss=self.model, **kwargs)
self.save() self.save()
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -109,7 +115,7 @@ class OperationSchema(LibraryItem):
return return
Argument.objects.filter(operation=target).delete() Argument.objects.filter(operation=target).delete()
SynthesisSubstitution.objects.filter(operation=target).delete() Substitution.objects.filter(operation=target).delete()
# trigger on_change effects # trigger on_change effects
@ -118,9 +124,9 @@ class OperationSchema(LibraryItem):
@transaction.atomic @transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]): def set_substitutions(self, target: Operation, substitutes: list[dict]):
''' Clear all arguments for operation. ''' ''' Clear all arguments for operation. '''
SynthesisSubstitution.objects.filter(operation=target).delete() Substitution.objects.filter(operation=target).delete()
for sub in substitutes: for sub in substitutes:
SynthesisSubstitution.objects.create( Substitution.objects.create(
operation=target, operation=target,
original=sub['original'], original=sub['original'],
substitution=sub['substitution'], substitution=sub['substitution'],

View File

@ -2,7 +2,7 @@
from django.db.models import CASCADE, BooleanField, ForeignKey, Model from django.db.models import CASCADE, BooleanField, ForeignKey, Model
class SynthesisSubstitution(Model): class Substitution(Model):
''' Substitutions as part of Synthesis operation in OSS.''' ''' Substitutions as part of Synthesis operation in OSS.'''
operation: ForeignKey = ForeignKey( operation: ForeignKey = ForeignKey(
verbose_name='Операция', verbose_name='Операция',

View File

@ -1,8 +1,6 @@
''' Django: Models. ''' ''' Django: Models. '''
from apps.rsform.models import LibraryItem, LibraryItemType
from .Argument import Argument from .Argument import Argument
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .SynthesisSubstitution import SynthesisSubstitution from .Substitution import Substitution

View File

@ -1,7 +1,5 @@
''' REST API: Serializers. ''' ''' REST API: Serializers. '''
from apps.rsform.serializers import LibraryItemSerializer
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
@ -10,4 +8,4 @@ from .data_access import (
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer OperationSerializer
) )
from .schema_typing import NewOperationResponse from .responses import NewOperationResponse

View File

@ -5,8 +5,8 @@ from django.db.models import F
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.rsform.models import LibraryItem from apps.library.models import LibraryItem
from apps.rsform.serializers import LibraryItemDetailsSerializer from apps.library.serializers import LibraryItemDetailsSerializer
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType from ..models import Argument, Operation, OperationSchema, OperationType
@ -85,19 +85,20 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = OperationSchema model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: OperationSchema): def to_representation(self, instance: LibraryItem):
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
oss = OperationSchema(instance)
result['items'] = [] result['items'] = []
for operation in instance.operations(): for operation in oss.operations():
result['items'].append(OperationSerializer(operation).data) result['items'].append(OperationSerializer(operation).data)
result['arguments'] = [] result['arguments'] = []
for argument in instance.arguments(): for argument in oss.arguments():
result['arguments'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = [] result['substitutions'] = []
for substitution in instance.substitutions().values( for substitution in oss.substitutions().values(
'operation', 'operation',
'original', 'original',
'substitution', 'substitution',

View File

@ -1,4 +1,4 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Argument import * from .t_Argument import *
from .t_Operation import * from .t_Operation import *
from .t_SynthesisSubstitution import * from .t_Substitution import *

View File

@ -8,11 +8,23 @@ class TestArgument(TestCase):
''' Testing Argument model. ''' ''' Testing Argument model. '''
def setUp(self): def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1') self.oss = OperationSchema.create(alias='T1')
self.operation1 = Operation.objects.create(oss=self.oss, alias='KS1', operation_type=OperationType.INPUT) self.operation1 = Operation.objects.create(
self.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS) oss=self.oss.model,
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT) alias='KS1',
operation_type=OperationType.INPUT
)
self.operation2 = Operation.objects.create(
oss=self.oss.model,
alias='KS2',
operation_type=OperationType.SYNTHESIS
)
self.operation3 = Operation.objects.create(
oss=self.oss.model,
alias='KS3',
operation_type=OperationType.INPUT
)
self.argument = Argument.objects.create( self.argument = Argument.objects.create(
operation=self.operation2, operation=self.operation2,
argument=self.operation1 argument=self.operation1

View File

@ -1,7 +1,8 @@
''' Testing models: Operation. ''' ''' Testing models: Operation. '''
from django.test import TestCase from django.test import TestCase
from apps.oss.models import LibraryItem, LibraryItemType, Operation, OperationSchema, OperationType from apps.library.models import LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
@ -9,9 +10,9 @@ class TestOperation(TestCase):
''' Testing Operation model. ''' ''' Testing Operation model. '''
def setUp(self): def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1') self.oss = OperationSchema.create(alias='T1')
self.operation = Operation.objects.create( self.operation = Operation.objects.create(
oss=self.oss, oss=self.oss.model,
alias='KS1' alias='KS1'
) )
@ -22,7 +23,7 @@ class TestOperation(TestCase):
def test_create_default(self): def test_create_default(self):
self.assertEqual(self.operation.oss, self.oss) self.assertEqual(self.operation.oss, self.oss.model)
self.assertEqual(self.operation.operation_type, OperationType.INPUT) self.assertEqual(self.operation.operation_type, OperationType.INPUT)
self.assertEqual(self.operation.result, None) self.assertEqual(self.operation.result, None)
self.assertEqual(self.operation.alias, 'KS1') self.assertEqual(self.operation.alias, 'KS1')
@ -34,29 +35,29 @@ class TestOperation(TestCase):
def test_sync_from_result(self): def test_sync_from_result(self):
schema = RSForm.objects.create(alias=self.operation.alias) schema = RSForm.create(alias=self.operation.alias)
self.operation.result = schema self.operation.result = schema.model
self.operation.save() self.operation.save()
schema.alias = 'KS2' schema.model.alias = 'KS2'
schema.comment = 'Comment' schema.model.comment = 'Comment'
schema.title = 'Title' schema.model.title = 'Title'
schema.save() schema.save()
self.operation.refresh_from_db() self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema) self.assertEqual(self.operation.result, schema.model)
self.assertEqual(self.operation.alias, schema.alias) self.assertEqual(self.operation.alias, schema.model.alias)
self.assertEqual(self.operation.title, schema.title) self.assertEqual(self.operation.title, schema.model.title)
self.assertEqual(self.operation.comment, schema.comment) self.assertEqual(self.operation.comment, schema.model.comment)
self.operation.sync_text = False self.operation.sync_text = False
self.operation.save() self.operation.save()
schema.alias = 'KS3' schema.model.alias = 'KS3'
schema.save() schema.save()
self.operation.refresh_from_db() self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema) self.assertEqual(self.operation.result, schema.model)
self.assertNotEqual(self.operation.alias, schema.alias) self.assertNotEqual(self.operation.alias, schema.model.alias)
def test_sync_from_library_item(self): def test_sync_from_library_item(self):
schema = LibraryItem.objects.create(alias=self.operation.alias, item_type=LibraryItemType.RSFORM) schema = LibraryItem.objects.create(alias=self.operation.alias, item_type=LibraryItemType.RSFORM)

View File

@ -3,13 +3,7 @@ from unittest import result
from django.test import TestCase from django.test import TestCase
from apps.oss.models import ( from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Substitution
Argument,
Operation,
OperationSchema,
OperationType,
SynthesisSubstitution
)
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
@ -17,24 +11,30 @@ class TestSynthesisSubstitution(TestCase):
''' Testing Synthesis Substitution model. ''' ''' Testing Synthesis Substitution model. '''
def setUp(self): def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1') self.oss = OperationSchema.create(alias='T1')
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1') self.ks1 = RSForm.create(alias='KS1', title='Test1')
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1') self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2') self.ks2 = RSForm.create(alias='KS2', title='Test2')
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2') self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
self.operation1 = Operation.objects.create( self.operation1 = Operation.objects.create(
oss=self.oss, oss=self.oss.model,
alias='KS1', alias='KS1',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks1) result=self.ks1.model
)
self.operation2 = Operation.objects.create( self.operation2 = Operation.objects.create(
oss=self.oss, oss=self.oss.model,
alias='KS2', alias='KS2',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks1) result=self.ks1.model
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.SYNTHESIS) )
self.operation3 = Operation.objects.create(
oss=self.oss.model,
alias='KS3',
operation_type=OperationType.SYNTHESIS
)
Argument.objects.create( Argument.objects.create(
operation=self.operation3, operation=self.operation3,
argument=self.operation1 argument=self.operation1
@ -44,7 +44,7 @@ class TestSynthesisSubstitution(TestCase):
argument=self.operation2 argument=self.operation2
) )
self.substitution = SynthesisSubstitution.objects.create( self.substitution = Substitution.objects.create(
operation=self.operation3, operation=self.operation3,
original=self.ks1x1, original=self.ks1x1,
substitution=self.ks2x1, substitution=self.ks2x1,
@ -58,18 +58,18 @@ class TestSynthesisSubstitution(TestCase):
def test_cascade_delete_operation(self): def test_cascade_delete_operation(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1) self.assertEqual(Substitution.objects.count(), 1)
self.operation3.delete() self.operation3.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0) self.assertEqual(Substitution.objects.count(), 0)
def test_cascade_delete_original(self): def test_cascade_delete_original(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1) self.assertEqual(Substitution.objects.count(), 1)
self.ks1x1.delete() self.ks1x1.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0) self.assertEqual(Substitution.objects.count(), 0)
def test_cascade_delete_substitution(self): def test_cascade_delete_substitution(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1) self.assertEqual(Substitution.objects.count(), 1)
self.ks2x1.delete() self.ks2x1.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0) self.assertEqual(Substitution.objects.count(), 0)

View File

@ -2,8 +2,9 @@
from rest_framework import status from rest_framework import status
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead, RSForm from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -12,29 +13,29 @@ class TestOssViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = OperationSchema.objects.create(title='Test', alias='T1', owner=self.user) self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.pk self.owned_id = self.owned.model.pk
self.unowned = OperationSchema.objects.create(title='Test2', alias='T2') self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.pk self.unowned_id = self.unowned.model.pk
self.private = OperationSchema.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.pk self.private_id = self.private.model.pk
self.invalid_id = self.private.pk + 1337 self.invalid_id = self.private.model.pk + 1337
def populateData(self): def populateData(self):
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1') self.ks1 = RSForm.create(alias='KS1', title='Test1', owner=self.user)
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1') self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2') self.ks2 = RSForm.create(alias='KS2', title='Test2', owner=self.user)
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2') self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
self.operation1 = self.owned.create_operation( self.operation1 = self.owned.create_operation(
alias='1', alias='1',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks1 result=self.ks1.model
) )
self.operation2 = self.owned.create_operation( self.operation2 = self.owned.create_operation(
alias='2', alias='2',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks2 result=self.ks2.model
) )
self.operation3 = self.owned.create_operation( self.operation3 = self.owned.create_operation(
alias='3', alias='3',
@ -53,12 +54,12 @@ class TestOssViewset(EndpointTester):
self.populateData() self.populateData()
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk) self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.title) self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.alias) self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.location) self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy) self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible) self.assertEqual(response.data['visible'], self.owned.model.visible)
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
@ -121,12 +122,12 @@ class TestOssViewset(EndpointTester):
self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y']) self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y'])
self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(data=data, item=self.private_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self): def test_create_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData() self.populateData()
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
@ -150,7 +151,9 @@ class TestOssViewset(EndpointTester):
self.executeBadData(data=data) self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT data['item_data']['operation_type'] = OperationType.INPUT
response = self.executeCreated(data=data) self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['items']), 4) self.assertEqual(len(response.data['oss']['items']), 4)
new_operation = response.data['new_operation'] new_operation = response.data['new_operation']
self.assertEqual(new_operation['alias'], data['item_data']['alias']) self.assertEqual(new_operation['alias'], data['item_data']['alias'])
@ -195,14 +198,14 @@ class TestOssViewset(EndpointTester):
'item_data': { 'item_data': {
'alias': 'Test4', 'alias': 'Test4',
'operation_type': OperationType.INPUT, 'operation_type': OperationType.INPUT,
'result': self.ks1.pk 'result': self.ks1.model.pk
}, },
'positions': [], 'positions': [],
} }
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.refresh_from_db()
new_operation = response.data['new_operation'] new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.pk) self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')

View File

@ -4,9 +4,9 @@ from rest_framework import routers
from . import views from . import views
library_router = routers.SimpleRouter(trailing_slash=False) oss_router = routers.SimpleRouter(trailing_slash=False)
library_router.register('oss', views.OssViewSet, 'OSS') oss_router.register('oss', views.OssViewSet, 'OSS')
urlpatterns = [ urlpatterns = [
path('', include(library_router.urls)), path('', include(oss_router.urls)),
] ]

View File

@ -10,6 +10,8 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer
from shared import permissions from shared import permissions
from .. import models as m from .. import models as m
@ -20,11 +22,11 @@ from .. import serializers as s
@extend_schema_view() @extend_schema_view()
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: OperationSchema. ''' ''' Endpoint: OperationSchema. '''
queryset = m.OperationSchema.objects.all() queryset = LibraryItem.objects.filter(item_type=LibraryItemType.OPERATION_SCHEMA)
serializer_class = s.LibraryItemSerializer serializer_class = LibraryItemSerializer
def _get_schema(self) -> m.OperationSchema: def _get_item(self) -> LibraryItem:
return cast(m.OperationSchema, self.get_object()) return cast(LibraryItem, self.get_object())
def get_permissions(self): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
@ -52,7 +54,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['get'], url_path='details') @action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk): def details(self, request: Request, pk):
''' Endpoint: Detailed OSS data. ''' ''' Endpoint: Detailed OSS data. '''
serializer = s.OperationSchemaSerializer(self._get_schema()) serializer = s.OperationSchemaSerializer(self._get_item())
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=serializer.data data=serializer.data
@ -71,10 +73,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='update-positions') @action(detail=True, methods=['patch'], url_path='update-positions')
def update_positions(self, request: Request, pk): def update_positions(self, request: Request, pk):
''' Endpoint: Update operations positions. ''' ''' Endpoint: Update operations positions. '''
schema = self._get_schema()
serializer = s.PositionsSerializer(data=request.data) serializer = s.PositionsSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.update_positions(serializer.validated_data['positions']) m.OperationSchema(self.get_object()).update_positions(serializer.validated_data['positions'])
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -91,23 +92,23 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-operation') @action(detail=True, methods=['post'], url_path='create-operation')
def create_operation(self, request: Request, pk): def create_operation(self, request: Request, pk):
''' Create new operation. ''' ''' Create new operation. '''
schema = self._get_schema()
serializer = s.OperationCreateSerializer(data=request.data) serializer = s.OperationCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
schema.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
new_operation = schema.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']: for argument in serializer.validated_data['arguments']:
schema.add_argument(operation=new_operation, argument=argument) oss.add_argument(operation=new_operation, argument=argument)
schema.refresh_from_db()
oss.refresh_from_db()
response = Response( response = Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': s.OperationSerializer(new_operation).data, 'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(schema).data 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )
return response return response
@ -126,19 +127,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation') @action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk): def delete_operation(self, request: Request, pk):
''' Endpoint: Delete operation. ''' ''' Endpoint: Delete operation. '''
schema = self._get_schema()
serializer = s.OperationDeleteSerializer( serializer = s.OperationDeleteSerializer(
data=request.data, data=request.data,
context={'oss': schema} context={'oss': self.get_object()}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
schema.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
schema.delete_operation(serializer.validated_data['target']) oss.delete_operation(serializer.validated_data['target'])
schema.refresh_from_db()
oss.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(schema).data data=s.OperationSchemaSerializer(oss.model).data
) )

View File

@ -10,60 +10,4 @@ class ConstituentaAdmin(admin.ModelAdmin):
list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved'] list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved']
search_fields = ['term_resolved', 'definition_resolved'] search_fields = ['term_resolved', 'definition_resolved']
class LibraryItemAdmin(admin.ModelAdmin):
''' Admin model: LibraryItem. '''
date_hierarchy = 'time_update'
list_display = [
'alias', 'title', 'owner',
'visible', 'read_only', 'access_policy', 'location',
'time_update'
]
list_filter = ['visible', 'read_only', 'access_policy', 'location', 'time_update']
search_fields = ['alias', 'title', 'location']
class LibraryTemplateAdmin(admin.ModelAdmin):
''' Admin model: LibraryTemplate. '''
list_display = ['id', 'alias']
list_select_related = ['lib_source']
def alias(self, template: models.LibraryTemplate):
if template.lib_source:
return template.lib_source.alias
else:
return 'N/A'
class SubscriptionAdmin(admin.ModelAdmin):
''' Admin model: Subscriptions. '''
list_display = ['id', 'item', 'user']
search_fields = [
'item__title', 'item__alias',
'user__username', 'user__first_name', 'user__last_name'
]
class EditorAdmin(admin.ModelAdmin):
''' Admin model: Editors. '''
list_display = ['id', 'item', 'editor']
search_fields = [
'item__title', 'item__alias',
'editor__username', 'editor__first_name', 'editor__last_name'
]
class VersionAdmin(admin.ModelAdmin):
''' Admin model: Versions. '''
list_display = ['id', 'item', 'version', 'description', 'time_create']
search_fields = [
'item__title', 'item__alias'
]
admin.site.register(models.Constituenta, ConstituentaAdmin) admin.site.register(models.Constituenta, ConstituentaAdmin)
admin.site.register(models.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
admin.site.register(models.Subscription, SubscriptionAdmin)
admin.site.register(models.Version, VersionAdmin)
admin.site.register(models.Editor, EditorAdmin)

View File

@ -1,10 +1,8 @@
# Generated by Django 4.2.4 on 2023-08-26 10:09 # Generated by Django 5.0.7 on 2024-07-25 16:06
import apps.rsform.models
from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -12,29 +10,10 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('library', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel(
name='LibraryItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_type', models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operations Schema')], max_length=50, verbose_name='Тип')),
('title', models.TextField(verbose_name='Название')),
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
('comment', models.TextField(blank=True, verbose_name='Комментарий')),
('is_common', models.BooleanField(default=False, verbose_name='Общая')),
('is_canonical', models.BooleanField(default=False, verbose_name='Каноничная')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')),
],
options={
'verbose_name': 'Схема',
'verbose_name_plural': 'Схемы',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Constituenta', name='Constituenta',
fields=[ fields=[
@ -45,28 +24,15 @@ class Migration(migrations.Migration):
('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')), ('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')),
('term_raw', models.TextField(blank=True, default='', verbose_name='Термин (с отсылками)')), ('term_raw', models.TextField(blank=True, default='', verbose_name='Термин (с отсылками)')),
('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')), ('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')),
('term_forms', models.JSONField(default=apps.rsform.models._empty_forms, verbose_name='Словоформы')), ('term_forms', models.JSONField(default=list, verbose_name='Словоформы')),
('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')), ('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')),
('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определние (с отсылками)')), ('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определение (с отсылками)')),
('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определние')), ('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определение')),
('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Концептуальная схема')), ('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Концептуальная схема')),
], ],
options={ options={
'verbose_name': 'Конституента', 'verbose_name': 'Конституента',
'verbose_name_plural': 'Конституенты', 'verbose_name_plural': 'Конституенты',
}, },
), ),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Подписки',
'verbose_name_plural': 'Подписка',
'unique_together': {('user', 'item')},
},
),
] ]

View File

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

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-27 08:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0002_librarytemplate'),
]
operations = [
migrations.AlterField(
model_name='constituenta',
name='definition_raw',
field=models.TextField(blank=True, default='', verbose_name='Текстовое определение (с отсылками)'),
),
migrations.AlterField(
model_name='constituenta',
name='definition_resolved',
field=models.TextField(blank=True, default='', verbose_name='Текстовое определение'),
),
]

View File

@ -1,30 +0,0 @@
# 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

@ -1,21 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 14:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('rsform', '0004_version'),
]
operations = [
migrations.AlterModelOptions(
name='subscription',
options={'verbose_name': 'Подписка', 'verbose_name_plural': 'Подписки'},
),
migrations.AlterModelOptions(
name='version',
options={'verbose_name': 'Версия', 'verbose_name_plural': 'Версии'},
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 14:40
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0005_alter_subscription_options_alter_version_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Editor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
('editor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, 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', 'editor')},
},
),
]

View File

@ -1,65 +0,0 @@
# Hand written migration 20240531
from django.db import migrations, models
from .. import models as m
class Migration(migrations.Migration):
dependencies = [
('rsform', '0006_editor'),
]
def calculate_location(apps, schema_editor):
LibraryItem = apps.get_model('rsform', 'LibraryItem')
db_alias = schema_editor.connection.alias
for item in LibraryItem.objects.using(db_alias).all():
if item.is_canonical:
location = m.LocationHead.LIBRARY
elif item.is_common:
location = m.LocationHead.COMMON
else:
location = m.LocationHead.USER
item.location = location
item.save(update_fields=['location'])
operations = [
migrations.AddField(
model_name='libraryitem',
name='access_policy',
field=models.CharField(
choices=[
('public', 'Public'),
('protected', 'Protected'),
('private', 'Private')
],
default='public',
max_length=500,
verbose_name='Политика доступа'),
),
migrations.AddField(
model_name='libraryitem',
name='location',
field=models.TextField(default='/U', max_length=500, verbose_name='Расположение'),
),
migrations.AddField(
model_name='libraryitem',
name='read_only',
field=models.BooleanField(default=False, verbose_name='Запретить редактирование'),
),
migrations.AddField(
model_name='libraryitem',
name='visible',
field=models.BooleanField(default=True, verbose_name='Отображаемая'),
),
migrations.RunPython(calculate_location, migrations.RunPython.noop), # type: ignore
migrations.RemoveField(
model_name='libraryitem',
name='is_canonical',
),
migrations.RemoveField(
model_name='libraryitem',
name='is_common',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-17 09:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0007_location_and_flags'),
]
operations = [
migrations.AlterField(
model_name='libraryitem',
name='item_type',
field=models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operation Schema')], max_length=50, verbose_name='Тип'),
),
]

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-22 14:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0008_alter_libraryitem_item_type'),
]
operations = [
migrations.CreateModel(
name='RSForm',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('rsform.libraryitem',),
),
migrations.AlterField(
model_name='constituenta',
name='schema',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема'),
),
migrations.AlterField(
model_name='librarytemplate',
name='lib_source',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Источник'),
),
]

View File

@ -20,10 +20,6 @@ _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line _GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
def _empty_forms():
return []
class CstType(TextChoices): class CstType(TextChoices):
''' Type of constituenta. ''' ''' Type of constituenta. '''
BASE = 'basic' BASE = 'basic'
@ -40,7 +36,7 @@ class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema. ''' ''' Constituenta is the base unit for every conceptual schema. '''
schema: ForeignKey = ForeignKey( schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема', verbose_name='Концептуальная схема',
to='rsform.RSForm', to='library.LibraryItem',
on_delete=CASCADE on_delete=CASCADE
) )
order: PositiveIntegerField = PositiveIntegerField( order: PositiveIntegerField = PositiveIntegerField(
@ -76,7 +72,7 @@ class Constituenta(Model):
) )
term_forms: JSONField = JSONField( term_forms: JSONField = JSONField(
verbose_name='Словоформы', verbose_name='Словоформы',
default=_empty_forms default=list
) )
definition_formal: TextField = TextField( definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение', verbose_name='Родоструктурное определение',

View File

@ -5,8 +5,9 @@ from typing import Optional, cast
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams from cctext import Entity, Resolver, TermForm, extract_entities, split_grams
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import Manager, QuerySet from django.db.models import QuerySet
from apps.library.models import LibraryItem, LibraryItemType, Version
from shared import messages as msg from shared import messages as msg
from ..graph import Graph from ..graph import Graph
@ -22,35 +23,39 @@ from .api_RSLanguage import (
split_template split_template
) )
from .Constituenta import Constituenta, CstType from .Constituenta import Constituenta, CstType
from .LibraryItem import LibraryItem, LibraryItemType
from .Version import Version
_INSERT_LAST: int = -1 _INSERT_LAST: int = -1
class RSForm(LibraryItem): class RSForm:
''' RSForm is math form of conceptual schema. ''' ''' RSForm is math form of conceptual schema. '''
class Meta: def __init__(self, model: LibraryItem):
''' Model metadata. ''' self.model = model
proxy = True
class InternalManager(Manager): @staticmethod
''' Object manager. ''' def create(**kwargs) -> 'RSForm':
''' Create LibraryItem via RSForm. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
return RSForm(model)
def get_queryset(self) -> QuerySet: @staticmethod
return super().get_queryset().filter(item_type=LibraryItemType.RSFORM) def from_id(pk: int) -> 'RSForm':
''' Get LibraryItem by pk. '''
model = LibraryItem.objects.get(pk=pk)
return RSForm(model)
def create(self, **kwargs): def save(self, *args, **kwargs):
kwargs.update({'item_type': LibraryItemType.RSFORM}) ''' Model wrapper. '''
return super().create(**kwargs) self.model.save(*args, **kwargs)
# Legit overriding object manager def refresh_from_db(self):
objects = InternalManager() # type: ignore[misc] ''' Model wrapper. '''
self.model.refresh_from_db()
def constituents(self) -> QuerySet[Constituenta]: def constituents(self) -> QuerySet[Constituenta]:
''' Get QuerySet containing all constituents of current RSForm. ''' ''' Get QuerySet containing all constituents of current RSForm. '''
return Constituenta.objects.filter(schema=self.pk) return Constituenta.objects.filter(schema=self.model)
def resolver(self) -> Resolver: def resolver(self) -> Resolver:
''' Create resolver for text references based on schema terms. ''' ''' Create resolver for text references based on schema terms. '''
@ -106,7 +111,7 @@ class RSForm(LibraryItem):
''' Get maximum alias index for specific CstType. ''' ''' Get maximum alias index for specific CstType. '''
result: int = 0 result: int = 0
items = Constituenta.objects \ items = Constituenta.objects \
.filter(schema=self, cst_type=cst_type) \ .filter(schema=self.model, cst_type=cst_type) \
.order_by('-alias') \ .order_by('-alias') \
.values_list('alias', flat=True) .values_list('alias', flat=True)
for alias in items: for alias in items:
@ -158,7 +163,7 @@ class RSForm(LibraryItem):
cst_type = guess_type(alias) cst_type = guess_type(alias)
self._shift_positions(position, 1) self._shift_positions(position, 1)
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self, schema=self.model,
order=position, order=position,
alias=alias, alias=alias,
cst_type=cst_type, cst_type=cst_type,
@ -191,7 +196,7 @@ class RSForm(LibraryItem):
result = deepcopy(items) result = deepcopy(items)
for cst in result: for cst in result:
cst.pk = None cst.pk = None
cst.schema = self cst.schema = self.model
cst.order = position cst.order = position
cst.alias = mapping[cst.alias] cst.alias = mapping[cst.alias]
cst.apply_mapping(mapping) cst.apply_mapping(mapping)
@ -304,7 +309,7 @@ class RSForm(LibraryItem):
def create_version(self, version: str, description: str, data) -> Version: def create_version(self, version: str, description: str, data) -> Version:
''' Creates version for current state. ''' ''' Creates version for current state. '''
return Version.objects.create( return Version.objects.create(
item=self, item=self.model,
version=version, version=version,
description=description, description=description,
data=data data=data
@ -330,7 +335,7 @@ class RSForm(LibraryItem):
prefix = get_type_prefix(cst_type) prefix = get_type_prefix(cst_type)
for text in expressions: for text in expressions:
new_item = Constituenta.objects.create( new_item = Constituenta.objects.create(
schema=self, schema=self.model,
order=position, order=position,
alias=f'{prefix}{free_index}', alias=f'{prefix}{free_index}',
definition_formal=text, definition_formal=text,
@ -349,7 +354,7 @@ class RSForm(LibraryItem):
update_list = \ update_list = \
Constituenta.objects \ Constituenta.objects \
.only('id', 'order', 'schema') \ .only('id', 'order', 'schema') \
.filter(schema=self.pk, order__gte=start) .filter(schema=self.model, order__gte=start)
for cst in update_list: for cst in update_list:
cst.order += shift cst.order += shift
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])

View File

@ -1,16 +1,4 @@
''' Django: Models. ''' ''' Django: Models. '''
from .Constituenta import Constituenta, CstType
from .RSForm import RSForm from .RSForm import RSForm
from .Constituenta import Constituenta, CstType, _empty_forms
from .Editor import Editor
from .LibraryItem import (
AccessPolicy,
LibraryItem,
LibraryItemType,
LocationHead,
User,
validate_location
)
from .LibraryTemplate import LibraryTemplate
from .Subscription import Subscription
from .Version import Version

View File

@ -1,11 +1,9 @@
''' REST API: Serializers. ''' ''' REST API: Serializers. '''
from .basics import ( from .basics import (
AccessPolicySerializer,
ASTNodeSerializer, ASTNodeSerializer,
ExpressionParseSerializer, ExpressionParseSerializer,
ExpressionSerializer, ExpressionSerializer,
LocationSerializer,
MultiFormSerializer, MultiFormSerializer,
ResolverSerializer, ResolverSerializer,
TextSerializer, TextSerializer,
@ -20,22 +18,9 @@ from .data_access import (
CstSubstituteSerializer, CstSubstituteSerializer,
CstTargetSerializer, CstTargetSerializer,
InlineSynthesisSerializer, InlineSynthesisSerializer,
LibraryItemBaseSerializer,
LibraryItemCloneSerializer,
LibraryItemDetailsSerializer,
LibraryItemSerializer,
RSFormParseSerializer, RSFormParseSerializer,
RSFormSerializer, RSFormSerializer
UsersListSerializer,
UserTargetSerializer,
VersionCreateSerializer,
VersionSerializer
) )
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter
from .schema_typing import ( from .responses import NewCstResponse, NewMultiCstResponse, ResultTextResponse
NewCstResponse,
NewMultiCstResponse,
NewVersionResponse,
ResultTextResponse
)

View File

@ -4,10 +4,6 @@ from typing import cast
from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference
from rest_framework import serializers from rest_framework import serializers
from shared import messages as msg
from ..models import AccessPolicy, validate_location
class ExpressionSerializer(serializers.Serializer): class ExpressionSerializer(serializers.Serializer):
''' Serializer: RSLang expression. ''' ''' Serializer: RSLang expression. '''
@ -20,32 +16,6 @@ class WordFormSerializer(serializers.Serializer):
grams = serializers.CharField() grams = serializers.CharField()
class LocationSerializer(serializers.Serializer):
''' Serializer: Item location. '''
location = serializers.CharField(max_length=500)
def validate(self, attrs):
attrs = super().validate(attrs)
if not validate_location(attrs['location']):
raise serializers.ValidationError({
'location': msg.invalidLocation()
})
return attrs
class AccessPolicySerializer(serializers.Serializer):
''' Serializer: Constituenta renaming. '''
access_policy = serializers.CharField()
def validate(self, attrs):
attrs = super().validate(attrs)
if not attrs['access_policy'] in AccessPolicy.values:
raise serializers.ValidationError({
'access_policy': msg.invalidEnum(attrs['access_policy'])
})
return attrs
class MultiFormSerializer(serializers.Serializer): class MultiFormSerializer(serializers.Serializer):
''' Serializer: inflect request. ''' ''' Serializer: inflect request. '''
items = serializers.ListField( items = serializers.ListField(

View File

@ -7,89 +7,15 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.library.models import LibraryItem
from apps.library.serializers import LibraryItemBaseSerializer, LibraryItemDetailsSerializer
from shared import messages as msg from shared import messages as msg
from ..models import Constituenta, CstType, LibraryItem, RSForm, Version from ..models import Constituenta, CstType, RSForm
from .basics import CstParseSerializer from .basics import CstParseSerializer
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter
class LibraryItemBaseSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry full access. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('id',)
class LibraryItemSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry limited access. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy')
class LibraryItemCloneSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem cloning. '''
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
class Meta:
''' serializer metadata. '''
model = LibraryItem
exclude = ['id', 'item_type', 'owner']
class VersionSerializer(serializers.ModelSerializer):
''' Serializer: Version data. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'id', 'version', 'item', 'description', 'time_create'
read_only_fields = ('id', 'item', 'time_create')
class VersionInnerSerializer(serializers.ModelSerializer):
''' Serializer: Version data for list of versions. '''
class Meta:
''' serializer metadata. '''
model = Version
fields = 'id', 'version', 'description', 'time_create'
read_only_fields = ('id', 'item', '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()
editors = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField()
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('owner', 'id', 'item_type')
def get_subscribers(self, instance: LibraryItem) -> list[int]:
return [item.pk for item in instance.subscribers()]
def get_editors(self, instance: LibraryItem) -> list[int]:
return [item.pk for item in instance.editors()]
def get_versions(self, instance: LibraryItem) -> list:
return [VersionInnerSerializer(item).data for item in instance.versions()]
class CstBaseSerializer(serializers.ModelSerializer): class CstBaseSerializer(serializers.ModelSerializer):
''' Serializer: Constituenta all data. ''' ''' Serializer: Constituenta all data. '''
class Meta: class Meta:
@ -112,18 +38,19 @@ class CstSerializer(serializers.ModelSerializer):
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None
term: Optional[str] = data['term_raw'] if 'term_raw' in data else None term: Optional[str] = data['term_raw'] if 'term_raw' in data else None
term_changed = 'term_forms' in data term_changed = 'term_forms' in data
schema = RSForm(instance.schema)
if definition is not None and definition != instance.definition_raw: if definition is not None and definition != instance.definition_raw:
data['definition_resolved'] = instance.schema.resolver().resolve(definition) data['definition_resolved'] = schema.resolver().resolve(definition)
if term is not None and term != instance.term_raw: if term is not None and term != instance.term_raw:
data['term_resolved'] = instance.schema.resolver().resolve(term) data['term_resolved'] = schema.resolver().resolve(term)
if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data: if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data:
data['term_forms'] = [] data['term_forms'] = []
term_changed = data['term_resolved'] != instance.term_resolved term_changed = data['term_resolved'] != instance.term_resolved
result: Constituenta = super().update(instance, data) result: Constituenta = super().update(instance, data)
if term_changed: if term_changed:
instance.schema.on_term_change([result.id]) schema.on_term_change([result.id])
result.refresh_from_db() result.refresh_from_db()
instance.schema.save() schema.save()
return result return result
@ -169,16 +96,16 @@ class RSFormSerializer(serializers.ModelSerializer):
model = LibraryItem model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: RSForm) -> dict: def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
result['items'] = [] result['items'] = []
for cst in instance.constituents().order_by('order'): for cst in RSForm(instance).constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data) result['items'].append(CstSerializer(cst).data)
return result return result
def to_versioned_data(self) -> dict: def to_versioned_data(self) -> dict:
''' Create serializable version representation without redundant data. ''' ''' Create serializable version representation without redundant data. '''
result = self.to_representation(cast(RSForm, self.instance)) result = self.to_representation(cast(LibraryItem, self.instance))
del result['versions'] del result['versions']
del result['subscribers'] del result['subscribers']
del result['editors'] del result['editors']
@ -195,14 +122,14 @@ class RSFormSerializer(serializers.ModelSerializer):
def from_versioned_data(self, version: int, data: dict) -> dict: def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. ''' ''' Load data from version. '''
result = self.to_representation(cast(RSForm, self.instance)) result = self.to_representation(cast(LibraryItem, self.instance))
result['version'] = version result['version'] = version
return result | data return result | data
@transaction.atomic @transaction.atomic
def restore_from_version(self, data: dict): def restore_from_version(self, data: dict):
''' Load data from version. ''' ''' Load data from version. '''
schema = cast(RSForm, self.instance) schema = RSForm(cast(LibraryItem, self.instance))
items: list[dict] = data['items'] items: list[dict] = data['items']
ids: list[int] = [item['id'] for item in items] ids: list[int] = [item['id'] for item in items]
processed: list[int] = [] processed: list[int] = []
@ -256,13 +183,13 @@ class RSFormParseSerializer(serializers.ModelSerializer):
model = LibraryItem model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: RSForm): def to_representation(self, instance: LibraryItem):
result = RSFormSerializer(instance).data result = RSFormSerializer(instance).data
return self._parse_data(result) return self._parse_data(result)
def from_versioned_data(self, version: int, data: dict) -> dict: def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version and parse. ''' ''' Load data from version and parse. '''
item = cast(RSForm, self.instance) item = cast(LibraryItem, self.instance)
result = RSFormSerializer(item).from_versioned_data(version, data) result = RSFormSerializer(item).from_versioned_data(version, data)
return self._parse_data(result) return self._parse_data(result)
@ -281,7 +208,7 @@ class CstTargetSerializer(serializers.Serializer):
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.all())
def validate(self, attrs): def validate(self, attrs):
schema = cast(RSForm, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if schema and cst.schema != schema: if schema and cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -295,16 +222,6 @@ class CstTargetSerializer(serializers.Serializer):
return attrs return attrs
class UserTargetSerializer(serializers.Serializer):
''' Serializer: Target single User. '''
user = PKField(many=False, queryset=User.objects.all())
class UsersListSerializer(serializers.Serializer):
''' Serializer: List of Users. '''
users = PKField(many=True, queryset=User.objects.all())
class CstRenameSerializer(serializers.Serializer): class CstRenameSerializer(serializers.Serializer):
''' Serializer: Constituenta renaming. ''' ''' Serializer: Constituenta renaming. '''
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.all())
@ -313,7 +230,7 @@ class CstRenameSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
attrs = super().validate(attrs) attrs = super().validate(attrs)
schema = cast(RSForm, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if cst.schema != schema: if cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -324,7 +241,7 @@ class CstRenameSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.renameTrivial(new_alias) 'alias': msg.renameTrivial(new_alias)
}) })
if schema.constituents().filter(alias=new_alias).exists(): if RSForm(schema).constituents().filter(alias=new_alias).exists():
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.aliasTaken(new_alias) 'alias': msg.aliasTaken(new_alias)
}) })
@ -336,7 +253,7 @@ class CstListSerializer(serializers.Serializer):
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
def validate(self, attrs): def validate(self, attrs):
schema = cast(RSForm, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
if not schema: if not schema:
return attrs return attrs
@ -368,7 +285,7 @@ class CstSubstituteSerializer(serializers.Serializer):
) )
def validate(self, attrs): def validate(self, attrs):
schema = cast(RSForm, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
deleted = set() deleted = set()
for item in attrs['substitutions']: for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original']) original_cst = cast(Constituenta, item['original'])
@ -395,8 +312,8 @@ class CstSubstituteSerializer(serializers.Serializer):
class InlineSynthesisSerializer(serializers.Serializer): class InlineSynthesisSerializer(serializers.Serializer):
''' Serializer: Inline synthesis operation input. ''' ''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=RSForm.objects.all()) receiver = PKField(many=False, queryset=LibraryItem.objects.all())
source = PKField(many=False, queryset=RSForm.objects.all()) # type: ignore source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=CstSubstituteSerializerBase() child=CstSubstituteSerializerBase()
@ -404,8 +321,8 @@ class InlineSynthesisSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
user = cast(User, self.context['user']) user = cast(User, self.context['user'])
schema_in = cast(RSForm, attrs['source']) schema_in = cast(LibraryItem, attrs['source'])
schema_out = cast(RSForm, attrs['receiver']) schema_out = cast(LibraryItem, attrs['receiver'])
if user.is_anonymous or (schema_out.owner != user and not user.is_staff): if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
raise PermissionDenied({ raise PermissionDenied({
'message': msg.schemaNotOwned(), 'message': msg.schemaNotOwned(),

View File

@ -2,6 +2,7 @@
from django.db import transaction from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from apps.library.models import LibraryItem
from shared import messages as msg from shared import messages as msg
from ..models import Constituenta, RSForm from ..models import Constituenta, RSForm
@ -29,14 +30,14 @@ class RSFormTRSSerializer(serializers.Serializer):
''' Serializer: TRS file production and loading for RSForm. ''' ''' Serializer: TRS file production and loading for RSForm. '''
def to_representation(self, instance: RSForm) -> dict: def to_representation(self, instance: RSForm) -> dict:
result = self._prepare_json_rsform(instance) result = self._prepare_json_rsform(instance.model)
items = instance.constituents().order_by('order') items = instance.constituents().order_by('order')
for cst in items: for cst in items:
result['items'].append(self._prepare_json_constituenta(cst)) result['items'].append(self._prepare_json_constituenta(cst))
return result return result
@staticmethod @staticmethod
def _prepare_json_rsform(schema: RSForm) -> dict: def _prepare_json_rsform(schema: LibraryItem) -> dict:
return { return {
'type': _TRS_TYPE, 'type': _TRS_TYPE,
'title': schema.title, 'title': schema.title,
@ -125,7 +126,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['comment'] = data.get('comment', '') result['comment'] = data.get('comment', '')
if 'id' in data: if 'id' in data:
result['id'] = data['id'] result['id'] = data['id']
self.instance = RSForm.objects.get(pk=result['id']) self.instance = RSForm.from_id(result['id'])
return result return result
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -139,7 +140,7 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def create(self, validated_data: dict) -> RSForm: def create(self, validated_data: dict) -> RSForm:
self.instance: RSForm = RSForm.objects.create( self.instance: RSForm = RSForm.create(
owner=validated_data.get('owner', None), owner=validated_data.get('owner', None),
alias=validated_data['alias'], alias=validated_data['alias'],
title=validated_data['title'], title=validated_data['title'],
@ -154,7 +155,7 @@ class RSFormTRSSerializer(serializers.Serializer):
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=self.instance, schema=self.instance.model,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -167,11 +168,11 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def update(self, instance: RSForm, validated_data) -> RSForm: def update(self, instance: RSForm, validated_data) -> RSForm:
if 'alias' in validated_data: if 'alias' in validated_data:
instance.alias = validated_data['alias'] instance.model.alias = validated_data['alias']
if 'title' in validated_data: if 'title' in validated_data:
instance.title = validated_data['title'] instance.model.title = validated_data['title']
if 'comment' in validated_data: if 'comment' in validated_data:
instance.comment = validated_data['comment'] instance.model.comment = validated_data['comment']
order = 1 order = 1
prev_constituents = instance.constituents() prev_constituents = instance.constituents()
@ -188,7 +189,7 @@ class RSFormTRSSerializer(serializers.Serializer):
else: else:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=instance, schema=instance.model,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )

View File

@ -21,9 +21,3 @@ class NewMultiCstResponse(serializers.Serializer):
child=serializers.IntegerField() child=serializers.IntegerField()
) )
schema = RSFormParseSerializer() schema = RSFormParseSerializer()
class NewVersionResponse(serializers.Serializer):
''' Serializer: Create cst response. '''
version = serializers.IntegerField()
schema = RSFormParseSerializer()

View File

@ -1,6 +1,3 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Constituenta import * from .t_Constituenta import *
from .t_Editor import *
from .t_LibraryItem import *
from .t_RSForm import * from .t_RSForm import *
from .t_Subscription import *

View File

@ -3,42 +3,42 @@ from django.db.utils import IntegrityError
from django.forms import ValidationError from django.forms import ValidationError
from django.test import TestCase from django.test import TestCase
from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm from apps.rsform.models import Constituenta, CstType, RSForm
class TestConstituenta(TestCase): class TestConstituenta(TestCase):
''' Testing Constituenta model. ''' ''' Testing Constituenta model. '''
def setUp(self): def setUp(self):
self.schema1 = RSForm.objects.create(title='Test1') self.schema1 = RSForm.create(title='Test1')
self.schema2 = RSForm.objects.create(title='Test2') self.schema2 = RSForm.create(title='Test2')
def test_str(self): def test_str(self):
testStr = 'X1' testStr = 'X1'
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test')
self.assertEqual(str(cst), testStr) self.assertEqual(str(cst), testStr)
def test_url(self): def test_url(self):
testStr = 'X1' testStr = 'X1'
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test')
self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.pk}') self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.pk}')
def test_order_not_null(self): def test_order_not_null(self):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1) Constituenta.objects.create(alias='X1', schema=self.schema1.model)
def test_order_positive_integer(self): def test_order_positive_integer(self):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1, order=-1) Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=-1)
def test_order_min_value(self): def test_order_min_value(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cst = Constituenta.objects.create(alias='X1', schema=self.schema1, order=0) cst = Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=0)
cst.full_clean() cst.full_clean()
@ -50,10 +50,10 @@ class TestConstituenta(TestCase):
def test_create_default(self): def test_create_default(self):
cst = Constituenta.objects.create( cst = Constituenta.objects.create(
alias='X1', alias='X1',
schema=self.schema1, schema=self.schema1.model,
order=1 order=1
) )
self.assertEqual(cst.schema, self.schema1) self.assertEqual(cst.schema, self.schema1.model)
self.assertEqual(cst.order, 1) self.assertEqual(cst.order, 1)
self.assertEqual(cst.alias, 'X1') self.assertEqual(cst.alias, 'X1')
self.assertEqual(cst.cst_type, CstType.BASE) self.assertEqual(cst.cst_type, CstType.BASE)

View File

@ -2,7 +2,8 @@
from django.forms import ValidationError from django.forms import ValidationError
from django.test import TestCase from django.test import TestCase
from apps.rsform.models import Constituenta, CstType, RSForm, User from apps.rsform.models import Constituenta, CstType, RSForm
from apps.users.models import User
class TestRSForm(TestCase): class TestRSForm(TestCase):
@ -11,49 +12,49 @@ class TestRSForm(TestCase):
def setUp(self): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
self.schema = RSForm.objects.create(title='Test') self.schema = RSForm.create(title='Test')
self.assertNotEqual(self.user1, self.user2) self.assertNotEqual(self.user1, self.user2)
def test_constituents(self): def test_constituents(self):
schema1 = RSForm.objects.create(title='Test1') schema1 = RSForm.create(title='Test1')
schema2 = RSForm.objects.create(title='Test2') schema2 = RSForm.create(title='Test2')
self.assertFalse(schema1.constituents().exists()) self.assertFalse(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
Constituenta.objects.create(alias='X1', schema=schema1, order=1) Constituenta.objects.create(alias='X1', schema=schema1.model, order=1)
Constituenta.objects.create(alias='X2', schema=schema1, order=2) Constituenta.objects.create(alias='X2', schema=schema1.model, order=2)
self.assertTrue(schema1.constituents().exists()) self.assertTrue(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
self.assertEqual(schema1.constituents().count(), 2) self.assertEqual(schema1.constituents().count(), 2)
def test_get_max_index(self): def test_get_max_index(self):
schema1 = RSForm.objects.create(title='Test1') schema1 = RSForm.create(title='Test1')
Constituenta.objects.create(alias='X1', schema=schema1, order=1) Constituenta.objects.create(alias='X1', schema=schema1.model, order=1)
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1, order=2) Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.model, order=2)
self.assertEqual(schema1.get_max_index(CstType.BASE), 1) self.assertEqual(schema1.get_max_index(CstType.BASE), 1)
self.assertEqual(schema1.get_max_index(CstType.TERM), 2) self.assertEqual(schema1.get_max_index(CstType.TERM), 2)
self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0) self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0)
def test_insert_at(self): def test_insert_at(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_new('X1') x1 = schema.insert_new('X1')
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(x1.schema, schema) self.assertEqual(x1.schema, schema.model)
x2 = schema.insert_new('X2', position=1) x2 = schema.insert_new('X2', position=1)
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x2.schema, schema) self.assertEqual(x2.schema, schema.model)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
x3 = schema.insert_new('X3', position=4) x3 = schema.insert_new('X3', position=4)
x2.refresh_from_db() x2.refresh_from_db()
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
self.assertEqual(x3.schema, schema) self.assertEqual(x3.schema, schema.model)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
@ -62,7 +63,7 @@ class TestRSForm(TestCase):
x2.refresh_from_db() x2.refresh_from_db()
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
self.assertEqual(x4.schema, schema) self.assertEqual(x4.schema, schema.model)
self.assertEqual(x3.order, 4) self.assertEqual(x3.order, 4)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
@ -94,11 +95,11 @@ class TestRSForm(TestCase):
def test_insert_last(self): def test_insert_last(self):
x1 = self.schema.insert_new('X1') x1 = self.schema.insert_new('X1')
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(x1.schema, self.schema) self.assertEqual(x1.schema, self.schema.model)
x2 = self.schema.insert_new('X2') x2 = self.schema.insert_new('X2')
self.assertEqual(x2.order, 2) self.assertEqual(x2.order, 2)
self.assertEqual(x2.schema, self.schema) self.assertEqual(x2.schema, self.schema.model)
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
def test_create_cst(self): def test_create_cst(self):

View File

@ -1,9 +1,6 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_library import * from .t_cctext import *
from .t_constituents import * from .t_constituents import *
from .t_operations import * from .t_operations import *
from .t_rsforms import * from .t_rsforms import *
from .t_versions import *
from .t_cctext import *
from .t_rslang import * from .t_rslang import *

View File

@ -8,12 +8,12 @@ class TestConstituentaAPI(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2') self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', alias='X1',
cst_type=CstType.BASE, cst_type=CstType.BASE,
schema=self.rsform_owned, schema=self.rsform_owned.model,
order=1, order=1,
convention='Test', convention='Test',
term_raw='Test1', term_raw='Test1',
@ -22,7 +22,7 @@ class TestConstituentaAPI(EndpointTester):
self.cst2 = Constituenta.objects.create( self.cst2 = Constituenta.objects.create(
alias='X2', alias='X2',
cst_type=CstType.BASE, cst_type=CstType.BASE,
schema=self.rsform_unowned, schema=self.rsform_unowned.model,
order=1, order=1,
convention='Test1', convention='Test1',
term_raw='Test2', term_raw='Test2',
@ -30,7 +30,7 @@ class TestConstituentaAPI(EndpointTester):
) )
self.cst3 = Constituenta.objects.create( self.cst3 = Constituenta.objects.create(
alias='X3', alias='X3',
schema=self.rsform_owned, schema=self.rsform_owned.model,
order=2, order=2,
term_raw='Test3', term_raw='Test3',
term_resolved='Test3', term_resolved='Test3',

View File

@ -10,16 +10,16 @@ class TestInlineSynthesis(EndpointTester):
@decl_endpoint('/api/operations/inline-synthesis', method='patch') @decl_endpoint('/api/operations/inline-synthesis', method='patch')
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.schema1 = RSForm.objects.create(title='Test1', alias='T1', owner=self.user) self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.objects.create(title='Test2', alias='T2', owner=self.user) self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.objects.create(title='Test3', alias='T3') self.unowned = RSForm.create(title='Test3', alias='T3')
def test_inline_synthesis_inputs(self): def test_inline_synthesis_inputs(self):
invalid_id = 1338 invalid_id = 1338
data = { data = {
'receiver': self.unowned.pk, 'receiver': self.unowned.model.pk,
'source': self.schema1.pk, 'source': self.schema1.model.pk,
'items': [], 'items': [],
'substitutions': [] 'substitutions': []
} }
@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester):
data['receiver'] = invalid_id data['receiver'] = invalid_id
self.executeBadData(data=data) self.executeBadData(data=data)
data['receiver'] = self.schema1.pk data['receiver'] = self.schema1.model.pk
data['source'] = invalid_id data['source'] = invalid_id
self.executeBadData(data=data) self.executeBadData(data=data)
data['source'] = self.schema1.pk data['source'] = self.schema1.model.pk
self.executeOK(data=data) self.executeOK(data=data)
data['items'] = [invalid_id] data['items'] = [invalid_id]
@ -51,8 +51,8 @@ class TestInlineSynthesis(EndpointTester):
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
data = { data = {
'receiver': self.schema1.pk, 'receiver': self.schema1.model.pk,
'source': self.schema2.pk, 'source': self.schema2.model.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [ 'substitutions': [
{ {

View File

@ -6,15 +6,8 @@ from zipfile import ZipFile
from cctext import ReferenceType from cctext import ReferenceType
from rest_framework import status from rest_framework import status
from apps.rsform.models import ( from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
AccessPolicy, from apps.rsform.models import Constituenta, CstType, RSForm
Constituenta,
CstType,
LibraryItem,
LibraryItemType,
LocationHead,
RSForm
)
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains from shared.testing_utils import response_contains
@ -24,12 +17,12 @@ class TestRSFormViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.pk self.owned_id = self.owned.model.pk
self.unowned = RSForm.objects.create(title='Test2', alias='T2') self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.pk self.unowned_id = self.unowned.model.pk
self.private = RSForm.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.pk self.private_id = self.private.model.pk
@decl_endpoint('/api/rsforms/create-detailed', method='post') @decl_endpoint('/api/rsforms/create-detailed', method='post')
@ -57,25 +50,25 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms', method='get') @decl_endpoint('/api/rsforms', method='get')
def test_list_rsforms(self): def test_list_rsforms(self):
non_schema = LibraryItem.objects.create( oss = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATION_SCHEMA, item_type=LibraryItemType.OPERATION_SCHEMA,
title='Test3' title='Test3'
) )
response = self.executeOK() response = self.executeOK()
self.assertFalse(response_contains(response, non_schema)) self.assertFalse(response_contains(response, oss))
self.assertTrue(response_contains(response, self.unowned)) self.assertTrue(response_contains(response, self.unowned.model))
self.assertTrue(response_contains(response, self.owned)) self.assertTrue(response_contains(response, self.owned.model))
@decl_endpoint('/api/rsforms/{item}/contents', method='get') @decl_endpoint('/api/rsforms/{item}/contents', method='get')
def test_contents(self): def test_contents(self):
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk) self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.title) self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.alias) self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.location) self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy) self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible) self.assertEqual(response.data['visible'], self.owned.model.visible)
@decl_endpoint('/api/rsforms/{item}/details', method='get') @decl_endpoint('/api/rsforms/{item}/details', method='get')
@ -92,12 +85,12 @@ class TestRSFormViewset(EndpointTester):
) )
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk) self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.title) self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.alias) self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.location) self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy) self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible) self.assertEqual(response.data['visible'], self.owned.model.visible)
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.pk) self.assertEqual(response.data['items'][0]['id'], x1.pk)
@ -176,9 +169,9 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/export-trs', method='get') @decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
def test_export_trs(self): def test_export_trs(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_new('X1') schema.insert_new('X1')
response = self.executeOK(item=schema.pk) response = self.executeOK(item=schema.model.pk)
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file: with ZipFile(stream, 'r') as zipped_file:
@ -458,7 +451,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/load-trs', method='patch') @decl_endpoint('/api/rsforms/{item}/load-trs', method='patch')
def test_load_trs(self): def test_load_trs(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
self.owned.title = 'Test11' self.owned.model.title = 'Test11'
self.owned.save() self.owned.save()
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_new('X1')
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
@ -467,7 +460,7 @@ class TestRSFormViewset(EndpointTester):
response = self.client.patch(self.endpoint, data=data, format='multipart') response = self.client.patch(self.endpoint, data=data, format='multipart')
self.owned.refresh_from_db() self.owned.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.owned.title, 'Test11') self.assertEqual(self.owned.model.title, 'Test11')
self.assertEqual(len(response.data['items']), 25) self.assertEqual(len(response.data['items']), 25)
self.assertEqual(self.owned.constituents().count(), 25) self.assertEqual(self.owned.constituents().count(), 25)
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())

View File

@ -5,22 +5,14 @@ from rest_framework import routers
from . import views from . import views
library_router = routers.SimpleRouter(trailing_slash=False) library_router = routers.SimpleRouter(trailing_slash=False)
library_router.register('library', views.LibraryViewSet, 'Library')
library_router.register('rsforms', views.RSFormViewSet, 'RSForm') library_router.register('rsforms', views.RSFormViewSet, 'RSForm')
library_router.register('versions', views.VersionViewset, 'Version')
urlpatterns = [ urlpatterns = [
path('library/active', views.LibraryActiveView.as_view()),
path('library/all', views.LibraryAdminView.as_view()),
path('library/templates', views.LibraryTemplatesView.as_view(), name='templates'),
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'), path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs', views.TrsImportView.as_view()), path('rsforms/import-trs', views.TrsImportView.as_view()),
path('rsforms/create-detailed', views.create_rsform), path('rsforms/create-detailed', views.create_rsform),
path('versions/<int:pk>/export-file', views.export_file),
path('rsforms/<int:pk_item>/versions/create', views.create_version),
path('rsforms/<int:pk_item>/versions/<int:pk_version>', views.retrieve_version),
path('operations/inline-synthesis', views.inline_synthesis), path('operations/inline-synthesis', views.inline_synthesis),
path('rslang/parse-expression', views.parse_expression), path('rslang/parse-expression', views.parse_expression),

View File

@ -1,8 +1,5 @@
''' Utility functions ''' ''' Utility functions '''
import json
import re import re
from io import BytesIO
from zipfile import ZipFile
# Name for JSON inside Exteor files archive # Name for JSON inside Exteor files archive
EXTEOR_INNER_FILENAME = 'document.json' EXTEOR_INNER_FILENAME = 'document.json'
@ -11,23 +8,6 @@ EXTEOR_INNER_FILENAME = 'document.json'
_REF_OLD_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)\|([^\}\|\{]*?)}') _REF_OLD_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)\|([^\}\|\{]*?)}')
def read_zipped_json(data, json_filename: str) -> dict:
''' Read JSON from zipped data '''
with ZipFile(data, 'r') as archive:
json_data = archive.read(json_filename)
result: dict = json.loads(json_data)
return result
def write_zipped_json(json_data: dict, json_filename: str) -> bytes:
''' Write json JSON to bytes buffer '''
content = BytesIO()
data = json.dumps(json_data, indent=4, ensure_ascii=False)
with ZipFile(content, 'w') as archive:
archive.writestr(json_filename, data=data)
return content.getvalue()
def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str: def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str:
''' Apply mapping to matching in regular expression pattern subgroup 1 ''' ''' Apply mapping to matching in regular expression pattern subgroup 1 '''
if text == '' or pattern == '': if text == '' or pattern == '':

View File

@ -1,8 +1,6 @@
''' REST API: Endpoint processors. ''' ''' REST API: Endpoint processors. '''
from .cctext import generate_lexeme, inflect, parse_text from .cctext import generate_lexeme, inflect, parse_text
from .constituents import ConstituentAPIView from .constituents import ConstituentAPIView
from .library import LibraryActiveView, LibraryAdminView, LibraryTemplatesView, LibraryViewSet
from .operations import inline_synthesis from .operations import inline_synthesis
from .rsforms import RSFormViewSet, TrsImportView, create_rsform from .rsforms import RSFormViewSet, TrsImportView, create_rsform
from .rslang import convert_to_ascii, convert_to_math, parse_expression from .rslang import convert_to_ascii, convert_to_math, parse_expression
from .versions import VersionViewset, create_version, export_file, retrieve_version

View File

@ -27,11 +27,11 @@ def inline_synthesis(request: Request):
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = cast(m.RSForm, serializer.validated_data['receiver']) receiver = m.RSForm(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items']) items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic(): with transaction.atomic():
new_items = schema.insert_copy(items) new_items = receiver.insert_copy(items)
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
@ -41,10 +41,10 @@ def inline_synthesis(request: Request):
else: else:
index = next(i for (i, cst) in enumerate(items) if cst == replacement) index = next(i for (i, cst) in enumerate(items) if cst == replacement)
replacement = new_items[index] replacement = new_items[index]
schema.substitute(original, replacement, substitution['transfer_term']) receiver.substitute(original, replacement, substitution['transfer_term'])
schema.restore_order() receiver.restore_order()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data data=s.RSFormParseSerializer(receiver.model).data
) )

View File

@ -13,8 +13,11 @@ from rest_framework.decorators import action, api_view
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.library.serializers import LibraryItemSerializer
from apps.users.models import User
from shared import messages as msg from shared import messages as msg
from shared import permissions from shared import permissions, utility
from .. import models as m from .. import models as m
from .. import serializers as s from .. import serializers as s
@ -25,11 +28,11 @@ from .. import utils
@extend_schema_view() @extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. ''' ''' Endpoint: RSForm operations. '''
queryset = m.RSForm.objects.all() queryset = LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM)
serializer_class = s.LibraryItemSerializer serializer_class = LibraryItemSerializer
def _get_schema(self) -> m.RSForm: def _get_item(self) -> LibraryItem:
return cast(m.RSForm, self.get_object()) return cast(LibraryItem, self.get_object())
def get_permissions(self): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
@ -71,18 +74,18 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['post'], url_path='create-cst') @action(detail=True, methods=['post'], url_path='create-cst')
def create_cst(self, request: Request, pk): def create_cst(self, request: Request, pk):
''' Create new constituenta. ''' ''' Create new constituenta. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
if 'insert_after' in data and data['insert_after'] is not None: if 'insert_after' in data and data['insert_after'] is not None:
try: try:
insert_after = m.Constituenta.objects.get(pk=data['insert_after']) insert_after = m.Constituenta.objects.get(pk=data['insert_after'])
except m.LibraryItem.DoesNotExist: except LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_404_NOT_FOUND)
else: else:
insert_after = None insert_after = None
new_cst = schema.create_cst(data, insert_after) new_cst = m.RSForm(schema).create_cst(data, insert_after)
schema.refresh_from_db() schema.refresh_from_db()
response = Response( response = Response(
@ -109,7 +112,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='produce-structure') @action(detail=True, methods=['patch'], url_path='produce-structure')
def produce_structure(self, request: Request, pk): def produce_structure(self, request: Request, pk):
''' Produce a term for every element of the target constituenta typification. ''' ''' Produce a term for every element of the target constituenta typification. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema}) serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -123,7 +126,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data={f'{cst.id}': msg.constituentaNoStructure()} data={f'{cst.id}': msg.constituentaNoStructure()}
) )
result = schema.produce_structure(cst, cst_parse) result = m.RSForm(schema).produce_structure(cst, cst_parse)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
@ -146,7 +149,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='rename-cst') @action(detail=True, methods=['patch'], url_path='rename-cst')
def rename_cst(self, request: Request, pk): def rename_cst(self, request: Request, pk):
''' Rename constituenta possibly changing type. ''' ''' Rename constituenta possibly changing type. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema}) serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -158,10 +161,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic(): with transaction.atomic():
cst.save() cst.save()
schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False) m.RSForm(schema).apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
schema.refresh_from_db() schema.refresh_from_db()
cst.refresh_from_db() cst.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
@ -184,7 +187,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='substitute') @action(detail=True, methods=['patch'], url_path='substitute')
def substitute(self, request: Request, pk): def substitute(self, request: Request, pk):
''' Substitute occurrences of constituenta with another one. ''' ''' Substitute occurrences of constituenta with another one. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstSubstituteSerializer( serializer = s.CstSubstituteSerializer(
data=request.data, data=request.data,
context={'schema': schema} context={'schema': schema}
@ -195,7 +198,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
schema.substitute(original, replacement, substitution['transfer_term']) m.RSForm(schema).substitute(original, replacement, substitution['transfer_term'])
schema.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -216,13 +220,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst') @action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
def delete_multiple_cst(self, request: Request, pk): def delete_multiple_cst(self, request: Request, pk):
''' Endpoint: Delete multiple constituents. ''' ''' Endpoint: Delete multiple constituents. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstListSerializer( serializer = s.CstListSerializer(
data=request.data, data=request.data,
context={'schema': schema} context={'schema': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.delete_cst(serializer.validated_data['items']) m.RSForm(schema).delete_cst(serializer.validated_data['items'])
schema.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -243,13 +248,13 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='move-cst') @action(detail=True, methods=['patch'], url_path='move-cst')
def move_cst(self, request: Request, pk): def move_cst(self, request: Request, pk):
''' Endpoint: Move multiple constituents. ''' ''' Endpoint: Move multiple constituents. '''
schema = self._get_schema() schema = self._get_item()
serializer = s.CstMoveSerializer( serializer = s.CstMoveSerializer(
data=request.data, data=request.data,
context={'schema': schema} context={'schema': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.move_cst( m.RSForm(schema).move_cst(
listCst=serializer.validated_data['items'], listCst=serializer.validated_data['items'],
target=serializer.validated_data['move_to'] target=serializer.validated_data['move_to']
) )
@ -271,8 +276,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='reset-aliases') @action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request: Request, pk): def reset_aliases(self, request: Request, pk):
''' Endpoint: Recreate all aliases based on order. ''' ''' Endpoint: Recreate all aliases based on order. '''
schema = self._get_schema() schema = self._get_item()
schema.reset_aliases() m.RSForm(schema).reset_aliases()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data data=s.RSFormParseSerializer(schema).data
@ -291,8 +296,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='restore-order') @action(detail=True, methods=['patch'], url_path='restore-order')
def restore_order(self, request: Request, pk): def restore_order(self, request: Request, pk):
''' Endpoint: Restore order based on types and term graph. ''' ''' Endpoint: Restore order based on types and term graph. '''
schema = self._get_schema() schema = self._get_item()
schema.restore_order() m.RSForm(schema).restore_order()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data data=s.RSFormParseSerializer(schema).data
@ -314,9 +319,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
''' Endpoint: Load data from file and replace current schema. ''' ''' Endpoint: Load data from file and replace current schema. '''
input_serializer = s.RSFormUploadSerializer(data=request.data) input_serializer = s.RSFormUploadSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True) input_serializer.is_valid(raise_exception=True)
schema = self._get_schema()
schema = self._get_item()
load_metadata = input_serializer.validated_data['load_metadata'] load_metadata = input_serializer.validated_data['load_metadata']
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
data['id'] = schema.pk data['id'] = schema.pk
serializer = s.RSFormTRSSerializer( serializer = s.RSFormTRSSerializer(
@ -324,10 +330,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'load_meta': load_metadata} context={'load_meta': load_metadata}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
result = serializer.save() result: m.RSForm = serializer.save()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result).data data=s.RSFormParseSerializer(result.model).data
) )
@extend_schema( @extend_schema(
@ -342,10 +348,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='contents') @action(detail=True, methods=['get'], url_path='contents')
def contents(self, request: Request, pk): def contents(self, request: Request, pk):
''' Endpoint: View schema db contents (including constituents). ''' ''' Endpoint: View schema db contents (including constituents). '''
schema = s.RSFormSerializer(self.get_object()) serializer = s.RSFormSerializer(self.get_object())
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=schema.data data=serializer.data
) )
@extend_schema( @extend_schema(
@ -360,7 +366,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='details') @action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk): def details(self, request: Request, pk):
''' Endpoint: Detailed schema view including statuses and parse. ''' ''' Endpoint: Detailed schema view including statuses and parse. '''
serializer = s.RSFormParseSerializer(self._get_schema()) serializer = s.RSFormParseSerializer(self.get_object())
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=serializer.data data=serializer.data
@ -381,8 +387,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression'] expression = serializer.validated_data['expression']
schema = s.PyConceptAdapter(self._get_schema()) pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
result = pyconcept.check_expression(json.dumps(schema.data), expression) result = pyconcept.check_expression(json.dumps(pySchema.data), expression)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=json.loads(result) data=json.loads(result)
@ -403,7 +409,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text'] text = serializer.validated_data['text']
resolver = self._get_schema().resolver() resolver = m.RSForm(self.get_object()).resolver()
resolver.resolve(text) resolver.resolve(text)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -422,9 +428,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='export-trs') @action(detail=True, methods=['get'], url_path='export-trs')
def export_trs(self, request: Request, pk): def export_trs(self, request: Request, pk):
''' Endpoint: Download Exteor compatible file. ''' ''' Endpoint: Download Exteor compatible file. '''
data = s.RSFormTRSSerializer(self._get_schema()).data schema = self._get_item()
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) data = s.RSFormTRSSerializer(m.RSForm(schema)).data
filename = utils.filename_for_schema(self._get_schema().alias) file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(schema.alias)
response = HttpResponse(file, content_type='application/zip') response = HttpResponse(file, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}' response['Content-Disposition'] = f'attachment; filename={filename}'
return response return response
@ -440,33 +447,32 @@ class TrsImportView(views.APIView):
tags=['RSForm'], tags=['RSForm'],
request=s.FileSerializer, request=s.FileSerializer,
responses={ responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer, c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None c.HTTP_403_FORBIDDEN: None
} }
) )
def post(self, request: Request): def post(self, request: Request):
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
owner = cast(m.User, self.request.user) owner = cast(User, self.request.user)
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer( serializer = s.RSFormTRSSerializer(
data=data, data=data,
context={'load_meta': True} context={'load_meta': True}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema: m.RSForm = serializer.save()
result = s.LibraryItemSerializer(schema)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=result.data data=LibraryItemSerializer(schema.model).data
) )
@extend_schema( @extend_schema(
summary='create new RSForm empty or from file', summary='create new RSForm empty or from file',
tags=['RSForm'], tags=['RSForm'],
request=s.LibraryItemSerializer, request=LibraryItemSerializer,
responses={ responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer, c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None c.HTTP_403_FORBIDDEN: None
} }
@ -474,26 +480,25 @@ class TrsImportView(views.APIView):
@api_view(['POST']) @api_view(['POST'])
def create_rsform(request: Request): def create_rsform(request: Request):
''' Endpoint: Create RSForm from user input and/or trs file. ''' ''' Endpoint: Create RSForm from user input and/or trs file. '''
owner = cast(m.User, request.user) if not request.user.is_anonymous else None owner = cast(User, request.user) if not request.user.is_anonymous else None
if 'file' not in request.FILES: if 'file' not in request.FILES:
return Response( return Response(
status=c.HTTP_400_BAD_REQUEST, status=c.HTTP_400_BAD_REQUEST,
data={'file': msg.missingFile()} data={'file': msg.missingFile()}
) )
else:
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True) serializer_rsform.is_valid(raise_exception=True)
schema = serializer_rsform.save() schema: m.RSForm = serializer_rsform.save()
result = s.LibraryItemSerializer(schema)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=result.data data=LibraryItemSerializer(schema.model).data
) )
def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None]): def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None]):
data['owner'] = owner data['owner'] = owner
if 'title' in request.data and request.data['title'] != '': if 'title' in request.data and request.data['title'] != '':
data['title'] = request.data['title'] data['title'] = request.data['title']
@ -514,5 +519,5 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None
read_only = request.data['read_only'] == 'true' read_only = request.data['read_only'] == 'true'
data['read_only'] = read_only data['read_only'] = read_only
data['access_policy'] = request.data.get('access_policy', m.AccessPolicy.PUBLIC) data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC)
data['location'] = request.data.get('location', m.LocationHead.USER) data['location'] = request.data.get('location', LocationHead.USER)

View File

@ -3,7 +3,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers from rest_framework import serializers
from apps.rsform.models import Editor, Subscription from apps.library.models import Editor, Subscription
from shared import messages as msg from shared import messages as msg
from . import models from . import models

View File

@ -4668,7 +4668,7 @@
} }
}, },
{ {
"model": "rsform.editor", "model": "library.editor",
"pk": 2, "pk": 2,
"fields": { "fields": {
"item": 35, "item": 35,
@ -4677,7 +4677,7 @@
} }
}, },
{ {
"model": "rsform.subscription", "model": "library.subscription",
"pk": 11, "pk": 11,
"fields": { "fields": {
"user": 1, "user": 1,
@ -4685,7 +4685,7 @@
} }
}, },
{ {
"model": "rsform.subscription", "model": "library.subscription",
"pk": 12, "pk": 12,
"fields": { "fields": {
"user": 5, "user": 5,
@ -4693,7 +4693,7 @@
} }
}, },
{ {
"model": "rsform.subscription", "model": "library.subscription",
"pk": 13, "pk": 13,
"fields": { "fields": {
"user": 3, "user": 3,
@ -4701,7 +4701,7 @@
} }
}, },
{ {
"model": "rsform.subscription", "model": "library.subscription",
"pk": 14, "pk": 14,
"fields": { "fields": {
"user": 3, "user": 3,
@ -4709,7 +4709,7 @@
} }
}, },
{ {
"model": "rsform.libraryitem", "model": "library.libraryitem",
"pk": 34, "pk": 34,
"fields": { "fields": {
"item_type": "rsform", "item_type": "rsform",
@ -4726,7 +4726,7 @@
} }
}, },
{ {
"model": "rsform.libraryitem", "model": "library.libraryitem",
"pk": 35, "pk": 35,
"fields": { "fields": {
"item_type": "rsform", "item_type": "rsform",
@ -4743,7 +4743,7 @@
} }
}, },
{ {
"model": "rsform.libraryitem", "model": "library.libraryitem",
"pk": 36, "pk": 36,
"fields": { "fields": {
"item_type": "rsform", "item_type": "rsform",
@ -4760,7 +4760,7 @@
} }
}, },
{ {
"model": "rsform.libraryitem", "model": "library.libraryitem",
"pk": 37, "pk": 37,
"fields": { "fields": {
"item_type": "rsform", "item_type": "rsform",
@ -4777,7 +4777,7 @@
} }
}, },
{ {
"model": "rsform.librarytemplate", "model": "library.librarytemplate",
"pk": 1, "pk": 1,
"fields": { "fields": {
"lib_source": 34 "lib_source": 34

View File

@ -73,6 +73,7 @@ INSTALLED_APPS = [
'corsheaders', 'corsheaders',
'apps.users', 'apps.users',
'apps.library',
'apps.rsform', 'apps.rsform',
'apps.oss', 'apps.oss',

View File

@ -8,6 +8,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec
urlpatterns = [ urlpatterns = [
path('admin', admin.site.urls), path('admin', admin.site.urls),
path('api/', include('apps.library.urls')),
path('api/', include('apps.rsform.urls')), path('api/', include('apps.rsform.urls')),
path('api/', include('apps.oss.urls')), path('api/', include('apps.oss.urls')),
path('users/', include('apps.users.urls')), path('users/', include('apps.users.urls')),

View File

@ -2,7 +2,7 @@
from rest_framework import status from rest_framework import status
from rest_framework.test import APIClient, APIRequestFactory, APITestCase from rest_framework.test import APIClient, APIRequestFactory, APITestCase
from apps.rsform.models import Editor, LibraryItem from apps.library.models import Editor, LibraryItem
from apps.users.models import User from apps.users.models import User

View File

@ -11,15 +11,9 @@ from rest_framework.permissions import \
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
from apps.library.models import AccessPolicy, Editor, LibraryItem, Subscription, Version
from apps.oss.models import Operation from apps.oss.models import Operation
from apps.rsform.models import ( from apps.rsform.models import Constituenta
AccessPolicy,
Constituenta,
Editor,
LibraryItem,
Subscription,
Version
)
from apps.users.models import User from apps.users.models import User

View File

@ -1,6 +1,6 @@
''' Utilities for testing. ''' ''' Utilities for testing. '''
from apps.rsform.models import LibraryItem from apps.library.models import LibraryItem
def response_contains(response, item: LibraryItem) -> bool: def response_contains(response, item: LibraryItem) -> bool:

View File

@ -0,0 +1,21 @@
''' Utility functions. '''
import json
from io import BytesIO
from zipfile import ZipFile
def read_zipped_json(data, json_filename: str) -> dict:
''' Read JSON from zipped data '''
with ZipFile(data, 'r') as archive:
json_data = archive.read(json_filename)
result: dict = json.loads(json_data)
return result
def write_zipped_json(json_data: dict, json_filename: str) -> bytes:
''' Write json JSON to bytes buffer '''
content = BytesIO()
data = json.dumps(json_data, indent=4, ensure_ascii=False)
with ZipFile(content, 'w') as archive:
archive.writestr(json_filename, data=data)
return content.getvalue()

View File

@ -7,9 +7,10 @@ import {
ILibraryItem, ILibraryItem,
ILibraryUpdateData, ILibraryUpdateData,
ITargetAccessPolicy, ITargetAccessPolicy,
ITargetLocation ITargetLocation,
IVersionData
} from '@/models/library'; } from '@/models/library';
import { IRSFormCloneData, IRSFormData } from '@/models/rsform'; import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
import { ITargetUser, ITargetUsers } from '@/models/user'; import { ITargetUser, ITargetUsers } from '@/models/user';
import { import {
@ -113,3 +114,10 @@ export function deleteUnsubscribe(target: string, request: FrontAction) {
request: request request: request
}); });
} }
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
AxiosPost({
endpoint: `/api/library/${target}/create-version`,
request: request
});
}

View File

@ -2,7 +2,7 @@
* Endpoints: rsforms. * Endpoints: rsforms.
*/ */
import { ILibraryCreateData, ILibraryItem, IVersionData } from '@/models/library'; import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss'; import { ICstSubstituteData } from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
@ -13,8 +13,7 @@ import {
IProduceStructureResponse, IProduceStructureResponse,
IRSFormData, IRSFormData,
IRSFormUploadData, IRSFormUploadData,
ITargetCst, ITargetCst
IVersionCreatedResponse
} from '@/models/rsform'; } from '@/models/rsform';
import { IExpressionParse, IRSExpression } from '@/models/rslang'; import { IExpressionParse, IRSExpression } from '@/models/rslang';
@ -40,7 +39,7 @@ export function getRSFormDetails(target: string, version: string, request: Front
}); });
} else { } else {
AxiosGet({ AxiosGet({
endpoint: `/api/rsforms/${target}/versions/${version}`, endpoint: `/api/library/${target}/versions/${version}`,
request: request request: request
}); });
} }
@ -136,10 +135,3 @@ export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUpl
} }
}); });
} }
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
AxiosPost({
endpoint: `/api/rsforms/${target}/versions/create`,
request: request
});
}

View File

@ -11,6 +11,7 @@ import {
patchSetEditors, patchSetEditors,
patchSetLocation, patchSetLocation,
patchSetOwner, patchSetOwner,
postCreateVersion,
postSubscribe postSubscribe
} from '@/backend/library'; } from '@/backend/library';
import { patchInlineSynthesis } from '@/backend/operations'; import { patchInlineSynthesis } from '@/backend/operations';
@ -24,8 +25,7 @@ import {
patchRestoreOrder, patchRestoreOrder,
patchSubstituteConstituents, patchSubstituteConstituents,
patchUploadTRS, patchUploadTRS,
postCreateConstituenta, postCreateConstituenta
postCreateVersion
} from '@/backend/rsforms'; } from '@/backend/rsforms';
import { deleteVersion, patchRestoreVersion, patchVersion } from '@/backend/versions'; import { deleteVersion, patchRestoreVersion, patchVersion } from '@/backend/versions';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';