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. '''
item: ForeignKey = ForeignKey(
verbose_name='Схема',
to='rsform.LibraryItem',
to='library.LibraryItem',
on_delete=CASCADE
)
editor: ForeignKey = ForeignKey(

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ class Version(Model):
''' Library item version archive. '''
item: ForeignKey = ForeignKey(
verbose_name='Схема',
to='rsform.LibraryItem',
to='library.LibraryItem',
on_delete=CASCADE
)
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. '''
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):
@ -10,7 +11,8 @@ class TestEditor(TestCase):
def setUp(self):
self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2')
self.item = RSForm.objects.create(
self.item = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test',
alias='КС1',
owner=self.user1

View File

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

View File

@ -1,7 +1,8 @@
''' Testing models: Subscription. '''
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):

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

View File

@ -15,51 +15,72 @@ class TestVersionViews(EndpointTester):
def setUp(self):
super().setUp()
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.unowned = RSForm.objects.create(title='Test2', alias='T2')
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
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(
alias='X1',
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):
invalid_data = {'description': 'test'}
invalid_id = 1338
data = {'version': '1.0.0', 'description': 'test'}
self.executeNotFound(data=data, schema=invalid_id)
self.executeForbidden(data=data, schema=self.unowned.pk)
self.executeBadData(data=invalid_data, schema=self.owned.pk)
self.executeForbidden(data=data, schema=self.unowned_id)
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('schema' in response.data)
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):
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
invalid_id = version_id + 1337
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=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.x1.alias = 'X33'
self.x1.save()
response = self.executeOK(schema=self.owned.pk, version=version_id)
self.assertNotEqual(response.data['alias'], self.owned.alias)
response = self.executeOK(schema=self.owned_id, version=version_id)
self.assertNotEqual(response.data['alias'], self.owned.model.alias)
self.assertNotEqual(response.data['items'][0]['alias'], self.x1.alias)
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')
def test_access_version(self):
data = {'version': '1.0.0', 'description': 'test'}
@ -73,7 +94,7 @@ class TestVersionViews(EndpointTester):
response = self.executeOK()
self.assertEqual(response.data['version'], data['version'])
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'}
self.method = 'patch'
@ -95,25 +116,6 @@ class TestVersionViews(EndpointTester):
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')
def test_export_version(self):
invalid_id = 1338
@ -123,7 +125,7 @@ class TestVersionViews(EndpointTester):
response = self.executeOK(version=version_id)
self.assertEqual(
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 ZipFile(stream, 'r') as zipped_file:
@ -165,7 +167,7 @@ class TestVersionViews(EndpointTester):
def _create_version(self, data) -> int:
response = self.client.post(
f'/api/rsforms/{self.owned.pk}/versions/create',
f'/api/library/{self.owned_id}/create-version',
data=data, format='json'
)
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.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 .. import models as m
@ -73,7 +76,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
tags=['Library'],
request=s.LibraryItemCloneSerializer,
responses={
c.HTTP_201_CREATED: s.RSFormParseSerializer,
c.HTTP_201_CREATED: RSFormParseSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
@ -88,8 +91,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
if item.item_type != m.LibraryItemType.RSFORM:
return Response(status=c.HTTP_400_BAD_REQUEST)
schema = m.RSForm.objects.get(pk=item.pk)
clone = deepcopy(schema)
clone = deepcopy(item)
clone.pk = None
clone.owner = self.request.user
clone.title = serializer.validated_data['title']
@ -103,14 +105,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic():
clone.save()
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']:
cst.pk = None
cst.schema = clone
cst.save()
return Response(
status=c.HTTP_201_CREATED,
data=s.RSFormParseSerializer(clone).data
data=RSFormParseSerializer(clone).data
)
@extend_schema(
@ -127,7 +129,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
def subscribe(self, request: Request, pk):
''' Endpoint: Subscribe current user to 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)
@extend_schema(
@ -144,7 +146,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
def unsubscribe(self, request: Request, pk):
''' Endpoint: Unsubscribe current user from 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)
@extend_schema(
@ -184,7 +186,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
item = self._get_item()
serializer = s.AccessPolicySerializer(data=request.data)
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)
@extend_schema(
@ -286,7 +289,7 @@ class LibraryActiveView(generics.ListAPIView):
.filter(is_public) \
.filter(common_location).order_by('-time_update')
else:
user = cast(m.User, self.request.user)
user = cast(User, self.request.user)
# pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter(
(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.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 serializers as s
from .. import utils
@extend_schema(tags=['Version'])
@ -32,7 +34,7 @@ class VersionViewset(
summary='restore version data into current item',
request=None,
responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_200_OK: RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
@ -42,81 +44,10 @@ class VersionViewset(
''' Restore version data into current item. '''
version = cast(m.Version, self.get_object())
item = cast(m.LibraryItem, version.item)
schema = m.RSForm.objects.get(pk=item.pk)
s.RSFormSerializer(schema).restore_from_version(version.data)
RSFormSerializer(item).restore_from_version(version.data)
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).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
data=RSFormParseSerializer(item).data
)
@ -136,10 +67,79 @@ def export_file(request: Request, pk: int):
version = m.Version.objects.get(pk=pk)
except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
schema = m.RSForm.objects.get(pk=version.item.pk)
data = s.RSFormTRSSerializer(schema).from_versioned_data(version.data)
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
data = RSFormTRSSerializer(version.item).from_versioned_data(version.data)
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(data['alias'])
response = HttpResponse(file, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}'
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.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
class RsformConfig(AppConfig):
class OssConfig(AppConfig):
''' Application config. '''
default_auto_field = 'django.db.models.BigAutoField'
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
from django.db import migrations, models
@ -9,7 +9,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('rsform', '0008_alter_libraryitem_item_type'),
('library', '0001_initial'),
('rsform', '0001_initial'),
]
operations = [
@ -17,17 +18,15 @@ class Migration(migrations.Migration):
name='Operation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('operation_type', models.CharField(choices=[
('input', 'Input'), ('synthesis', 'Synthesis')], default='input', max_length=10, verbose_name='Тип')),
('operation_type', models.CharField(choices=[('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='Шифр')),
('title', models.TextField(blank=True, verbose_name='Название')),
('comment', models.TextField(blank=True, verbose_name='Комментарий')),
('position_x', models.FloatField(default=0, verbose_name='Положение по горизонтали')),
('position_y', models.FloatField(default=0, verbose_name='Положение по вертикали')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
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='rsform.libraryitem', verbose_name='Связанная КС')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='library.libraryitem', verbose_name='Схема синтеза')),
('result', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='producer', to='library.libraryitem', verbose_name='Связанная КС')),
],
options={
'verbose_name': 'Операция',
@ -35,16 +34,13 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='SynthesisSubstitution',
name='Substitution',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer_term', models.BooleanField(default=False, verbose_name='Перенос термина')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to='oss.operation', verbose_name='Операция')),
('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
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='Замещающая конституента')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oss.operation', verbose_name='Операция')),
('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 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={
'verbose_name': 'Отождествление синтеза',
@ -55,10 +51,8 @@ class Migration(migrations.Migration):
name='Argument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
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='Операция')),
('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 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='Операция')),
],
options={
'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.'''
oss: ForeignKey = ForeignKey(
verbose_name='Схема синтеза',
to='oss.OperationSchema',
to='library.LibraryItem',
on_delete=CASCADE,
related_name='items'
)
@ -34,7 +34,7 @@ class Operation(Model):
)
result: ForeignKey = ForeignKey(
verbose_name='Связанная КС',
to='rsform.RSForm',
to='library.LibraryItem',
null=True,
on_delete=SET_NULL,
related_name='producer'

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
''' REST API: Serializers. '''
from apps.rsform.serializers import LibraryItemSerializer
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
from .data_access import (
ArgumentSerializer,
@ -10,4 +8,4 @@ from .data_access import (
OperationSchemaSerializer,
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.serializers import PrimaryKeyRelatedField as PKField
from apps.rsform.models import LibraryItem
from apps.rsform.serializers import LibraryItemDetailsSerializer
from apps.library.models import LibraryItem
from apps.library.serializers import LibraryItemDetailsSerializer
from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType
@ -85,19 +85,20 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
class Meta:
''' serializer metadata. '''
model = OperationSchema
model = LibraryItem
fields = '__all__'
def to_representation(self, instance: OperationSchema):
def to_representation(self, instance: LibraryItem):
result = LibraryItemDetailsSerializer(instance).data
oss = OperationSchema(instance)
result['items'] = []
for operation in instance.operations():
for operation in oss.operations():
result['items'].append(OperationSerializer(operation).data)
result['arguments'] = []
for argument in instance.arguments():
for argument in oss.arguments():
result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = []
for substitution in instance.substitutions().values(
for substitution in oss.substitutions().values(
'operation',
'original',
'substitution',

View File

@ -1,4 +1,4 @@
''' Tests for Django Models. '''
from .t_Argument 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. '''
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.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS)
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT)
self.operation1 = Operation.objects.create(
oss=self.oss.model,
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(
operation=self.operation2,
argument=self.operation1

View File

@ -1,7 +1,8 @@
''' Testing models: Operation. '''
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
@ -9,9 +10,9 @@ class TestOperation(TestCase):
''' Testing Operation model. '''
def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1')
self.oss = OperationSchema.create(alias='T1')
self.operation = Operation.objects.create(
oss=self.oss,
oss=self.oss.model,
alias='KS1'
)
@ -22,7 +23,7 @@ class TestOperation(TestCase):
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.result, None)
self.assertEqual(self.operation.alias, 'KS1')
@ -34,29 +35,29 @@ class TestOperation(TestCase):
def test_sync_from_result(self):
schema = RSForm.objects.create(alias=self.operation.alias)
self.operation.result = schema
schema = RSForm.create(alias=self.operation.alias)
self.operation.result = schema.model
self.operation.save()
schema.alias = 'KS2'
schema.comment = 'Comment'
schema.title = 'Title'
schema.model.alias = 'KS2'
schema.model.comment = 'Comment'
schema.model.title = 'Title'
schema.save()
self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema)
self.assertEqual(self.operation.alias, schema.alias)
self.assertEqual(self.operation.title, schema.title)
self.assertEqual(self.operation.comment, schema.comment)
self.assertEqual(self.operation.result, schema.model)
self.assertEqual(self.operation.alias, schema.model.alias)
self.assertEqual(self.operation.title, schema.model.title)
self.assertEqual(self.operation.comment, schema.model.comment)
self.operation.sync_text = False
self.operation.save()
schema.alias = 'KS3'
schema.model.alias = 'KS3'
schema.save()
self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema)
self.assertNotEqual(self.operation.alias, schema.alias)
self.assertEqual(self.operation.result, schema.model)
self.assertNotEqual(self.operation.alias, schema.model.alias)
def test_sync_from_library_item(self):
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 apps.oss.models import (
Argument,
Operation,
OperationSchema,
OperationType,
SynthesisSubstitution
)
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Substitution
from apps.rsform.models import RSForm
@ -17,24 +11,30 @@ class TestSynthesisSubstitution(TestCase):
''' Testing Synthesis Substitution model. '''
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.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.operation1 = Operation.objects.create(
oss=self.oss,
oss=self.oss.model,
alias='KS1',
operation_type=OperationType.INPUT,
result=self.ks1)
result=self.ks1.model
)
self.operation2 = Operation.objects.create(
oss=self.oss,
oss=self.oss.model,
alias='KS2',
operation_type=OperationType.INPUT,
result=self.ks1)
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.SYNTHESIS)
result=self.ks1.model
)
self.operation3 = Operation.objects.create(
oss=self.oss.model,
alias='KS3',
operation_type=OperationType.SYNTHESIS
)
Argument.objects.create(
operation=self.operation3,
argument=self.operation1
@ -44,7 +44,7 @@ class TestSynthesisSubstitution(TestCase):
argument=self.operation2
)
self.substitution = SynthesisSubstitution.objects.create(
self.substitution = Substitution.objects.create(
operation=self.operation3,
original=self.ks1x1,
substitution=self.ks2x1,
@ -58,18 +58,18 @@ class TestSynthesisSubstitution(TestCase):
def test_cascade_delete_operation(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.assertEqual(Substitution.objects.count(), 1)
self.operation3.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
self.assertEqual(Substitution.objects.count(), 0)
def test_cascade_delete_original(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.assertEqual(Substitution.objects.count(), 1)
self.ks1x1.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
self.assertEqual(Substitution.objects.count(), 0)
def test_cascade_delete_substitution(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.assertEqual(Substitution.objects.count(), 1)
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 apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
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
@ -12,29 +13,29 @@ class TestOssViewset(EndpointTester):
def setUp(self):
super().setUp()
self.owned = OperationSchema.objects.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.pk
self.unowned = OperationSchema.objects.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.pk
self.private = OperationSchema.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.pk
self.invalid_id = self.private.pk + 1337
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.model.pk
self.invalid_id = self.private.model.pk + 1337
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.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.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1
result=self.ks1.model
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2
result=self.ks2.model
)
self.operation3 = self.owned.create_operation(
alias='3',
@ -53,12 +54,12 @@ class TestOssViewset(EndpointTester):
self.populateData()
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible)
self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.model.visible)
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.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')
def test_create_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
@ -150,7 +151,9 @@ class TestOssViewset(EndpointTester):
self.executeBadData(data=data)
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)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
@ -195,14 +198,14 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.pk
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
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')

View File

@ -4,9 +4,9 @@ from rest_framework import routers
from . import views
library_router = routers.SimpleRouter(trailing_slash=False)
library_router.register('oss', views.OssViewSet, 'OSS')
oss_router = routers.SimpleRouter(trailing_slash=False)
oss_router.register('oss', views.OssViewSet, 'OSS')
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.response import Response
from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer
from shared import permissions
from .. import models as m
@ -20,11 +22,11 @@ from .. import serializers as s
@extend_schema_view()
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: OperationSchema. '''
queryset = m.OperationSchema.objects.all()
serializer_class = s.LibraryItemSerializer
queryset = LibraryItem.objects.filter(item_type=LibraryItemType.OPERATION_SCHEMA)
serializer_class = LibraryItemSerializer
def _get_schema(self) -> m.OperationSchema:
return cast(m.OperationSchema, self.get_object())
def _get_item(self) -> LibraryItem:
return cast(LibraryItem, self.get_object())
def get_permissions(self):
''' Determine permission class. '''
@ -52,7 +54,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk):
''' Endpoint: Detailed OSS data. '''
serializer = s.OperationSchemaSerializer(self._get_schema())
serializer = s.OperationSchemaSerializer(self._get_item())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -71,10 +73,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='update-positions')
def update_positions(self, request: Request, pk):
''' Endpoint: Update operations positions. '''
schema = self._get_schema()
serializer = s.PositionsSerializer(data=request.data)
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)
@extend_schema(
@ -91,23 +92,23 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-operation')
def create_operation(self, request: Request, pk):
''' Create new operation. '''
schema = self._get_schema()
serializer = s.OperationCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
schema.update_positions(serializer.validated_data['positions'])
new_operation = schema.create_operation(**serializer.validated_data['item_data'])
oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']:
schema.add_argument(operation=new_operation, argument=argument)
schema.refresh_from_db()
oss.add_argument(operation=new_operation, argument=argument)
oss.refresh_from_db()
response = Response(
status=c.HTTP_201_CREATED,
data={
'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(schema).data
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
return response
@ -126,19 +127,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk):
''' Endpoint: Delete operation. '''
schema = self._get_schema()
serializer = s.OperationDeleteSerializer(
data=request.data,
context={'oss': schema}
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
schema.update_positions(serializer.validated_data['positions'])
schema.delete_operation(serializer.validated_data['target'])
schema.refresh_from_db()
oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(serializer.validated_data['target'])
oss.refresh_from_db()
return Response(
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']
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.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
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
@ -12,29 +10,10 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('library', '0001_initial'),
]
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(
name='Constituenta',
fields=[
@ -45,28 +24,15 @@ class Migration(migrations.Migration):
('convention', 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_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_raw', 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='Концептуальная схема')),
('definition_raw', 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='library.libraryitem', verbose_name='Концептуальная схема')),
],
options={
'verbose_name': 'Конституента',
'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
def _empty_forms():
return []
class CstType(TextChoices):
''' Type of constituenta. '''
BASE = 'basic'
@ -40,7 +36,7 @@ class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema. '''
schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to='rsform.RSForm',
to='library.LibraryItem',
on_delete=CASCADE
)
order: PositiveIntegerField = PositiveIntegerField(
@ -76,7 +72,7 @@ class Constituenta(Model):
)
term_forms: JSONField = JSONField(
verbose_name='Словоформы',
default=_empty_forms
default=list
)
definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение',

View File

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

View File

@ -1,16 +1,4 @@
''' Django: Models. '''
from .Constituenta import Constituenta, CstType
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. '''
from .basics import (
AccessPolicySerializer,
ASTNodeSerializer,
ExpressionParseSerializer,
ExpressionSerializer,
LocationSerializer,
MultiFormSerializer,
ResolverSerializer,
TextSerializer,
@ -20,22 +18,9 @@ from .data_access import (
CstSubstituteSerializer,
CstTargetSerializer,
InlineSynthesisSerializer,
LibraryItemBaseSerializer,
LibraryItemCloneSerializer,
LibraryItemDetailsSerializer,
LibraryItemSerializer,
RSFormParseSerializer,
RSFormSerializer,
UsersListSerializer,
UserTargetSerializer,
VersionCreateSerializer,
VersionSerializer
RSFormSerializer
)
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
from .io_pyconcept import PyConceptAdapter
from .schema_typing import (
NewCstResponse,
NewMultiCstResponse,
NewVersionResponse,
ResultTextResponse
)
from .responses import NewCstResponse, NewMultiCstResponse, ResultTextResponse

View File

@ -4,10 +4,6 @@ from typing import cast
from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference
from rest_framework import serializers
from shared import messages as msg
from ..models import AccessPolicy, validate_location
class ExpressionSerializer(serializers.Serializer):
''' Serializer: RSLang expression. '''
@ -20,32 +16,6 @@ class WordFormSerializer(serializers.Serializer):
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):
''' Serializer: inflect request. '''
items = serializers.ListField(

View File

@ -7,89 +7,15 @@ from django.db import transaction
from rest_framework import serializers
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 ..models import Constituenta, CstType, LibraryItem, RSForm, Version
from ..models import Constituenta, CstType, RSForm
from .basics import CstParseSerializer
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):
''' Serializer: Constituenta all data. '''
class Meta:
@ -112,18 +38,19 @@ class CstSerializer(serializers.ModelSerializer):
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_changed = 'term_forms' in data
schema = RSForm(instance.schema)
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:
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:
data['term_forms'] = []
term_changed = data['term_resolved'] != instance.term_resolved
result: Constituenta = super().update(instance, data)
if term_changed:
instance.schema.on_term_change([result.id])
schema.on_term_change([result.id])
result.refresh_from_db()
instance.schema.save()
schema.save()
return result
@ -169,16 +96,16 @@ class RSFormSerializer(serializers.ModelSerializer):
model = LibraryItem
fields = '__all__'
def to_representation(self, instance: RSForm) -> dict:
def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data
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)
return result
def to_versioned_data(self) -> dict:
''' 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['subscribers']
del result['editors']
@ -195,14 +122,14 @@ class RSFormSerializer(serializers.ModelSerializer):
def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. '''
result = self.to_representation(cast(RSForm, self.instance))
result = self.to_representation(cast(LibraryItem, self.instance))
result['version'] = version
return result | data
@transaction.atomic
def restore_from_version(self, data: dict):
''' Load data from version. '''
schema = cast(RSForm, self.instance)
schema = RSForm(cast(LibraryItem, self.instance))
items: list[dict] = data['items']
ids: list[int] = [item['id'] for item in items]
processed: list[int] = []
@ -256,13 +183,13 @@ class RSFormParseSerializer(serializers.ModelSerializer):
model = LibraryItem
fields = '__all__'
def to_representation(self, instance: RSForm):
def to_representation(self, instance: LibraryItem):
result = RSFormSerializer(instance).data
return self._parse_data(result)
def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version and parse. '''
item = cast(RSForm, self.instance)
item = cast(LibraryItem, self.instance)
result = RSFormSerializer(item).from_versioned_data(version, data)
return self._parse_data(result)
@ -281,7 +208,7 @@ class CstTargetSerializer(serializers.Serializer):
target = PKField(many=False, queryset=Constituenta.objects.all())
def validate(self, attrs):
schema = cast(RSForm, self.context['schema'])
schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target'])
if schema and cst.schema != schema:
raise serializers.ValidationError({
@ -295,16 +222,6 @@ class CstTargetSerializer(serializers.Serializer):
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):
''' Serializer: Constituenta renaming. '''
target = PKField(many=False, queryset=Constituenta.objects.all())
@ -313,7 +230,7 @@ class CstRenameSerializer(serializers.Serializer):
def validate(self, attrs):
attrs = super().validate(attrs)
schema = cast(RSForm, self.context['schema'])
schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target'])
if cst.schema != schema:
raise serializers.ValidationError({
@ -324,7 +241,7 @@ class CstRenameSerializer(serializers.Serializer):
raise serializers.ValidationError({
'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({
'alias': msg.aliasTaken(new_alias)
})
@ -336,7 +253,7 @@ class CstListSerializer(serializers.Serializer):
items = PKField(many=True, queryset=Constituenta.objects.all())
def validate(self, attrs):
schema = cast(RSForm, self.context['schema'])
schema = cast(LibraryItem, self.context['schema'])
if not schema:
return attrs
@ -368,7 +285,7 @@ class CstSubstituteSerializer(serializers.Serializer):
)
def validate(self, attrs):
schema = cast(RSForm, self.context['schema'])
schema = cast(LibraryItem, self.context['schema'])
deleted = set()
for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original'])
@ -395,8 +312,8 @@ class CstSubstituteSerializer(serializers.Serializer):
class InlineSynthesisSerializer(serializers.Serializer):
''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=RSForm.objects.all())
source = PKField(many=False, queryset=RSForm.objects.all()) # type: ignore
receiver = PKField(many=False, queryset=LibraryItem.objects.all())
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
items = PKField(many=True, queryset=Constituenta.objects.all())
substitutions = serializers.ListField(
child=CstSubstituteSerializerBase()
@ -404,8 +321,8 @@ class InlineSynthesisSerializer(serializers.Serializer):
def validate(self, attrs):
user = cast(User, self.context['user'])
schema_in = cast(RSForm, attrs['source'])
schema_out = cast(RSForm, attrs['receiver'])
schema_in = cast(LibraryItem, attrs['source'])
schema_out = cast(LibraryItem, attrs['receiver'])
if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
raise PermissionDenied({
'message': msg.schemaNotOwned(),

View File

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

View File

@ -21,9 +21,3 @@ class NewMultiCstResponse(serializers.Serializer):
child=serializers.IntegerField()
)
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. '''
from .t_Constituenta import *
from .t_Editor import *
from .t_LibraryItem 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.test import TestCase
from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm
from apps.rsform.models import Constituenta, CstType, RSForm
class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self):
self.schema1 = RSForm.objects.create(title='Test1')
self.schema2 = RSForm.objects.create(title='Test2')
self.schema1 = RSForm.create(title='Test1')
self.schema2 = RSForm.create(title='Test2')
def test_str(self):
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)
def test_url(self):
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}')
def test_order_not_null(self):
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):
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):
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()
@ -50,10 +50,10 @@ class TestConstituenta(TestCase):
def test_create_default(self):
cst = Constituenta.objects.create(
alias='X1',
schema=self.schema1,
schema=self.schema1.model,
order=1
)
self.assertEqual(cst.schema, self.schema1)
self.assertEqual(cst.schema, self.schema1.model)
self.assertEqual(cst.order, 1)
self.assertEqual(cst.alias, 'X1')
self.assertEqual(cst.cst_type, CstType.BASE)

View File

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

View File

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

View File

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

View File

@ -10,16 +10,16 @@ class TestInlineSynthesis(EndpointTester):
@decl_endpoint('/api/operations/inline-synthesis', method='patch')
def setUp(self):
super().setUp()
self.schema1 = RSForm.objects.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.objects.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.objects.create(title='Test3', alias='T3')
self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.create(title='Test3', alias='T3')
def test_inline_synthesis_inputs(self):
invalid_id = 1338
data = {
'receiver': self.unowned.pk,
'source': self.schema1.pk,
'receiver': self.unowned.model.pk,
'source': self.schema1.model.pk,
'items': [],
'substitutions': []
}
@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester):
data['receiver'] = invalid_id
self.executeBadData(data=data)
data['receiver'] = self.schema1.pk
data['receiver'] = self.schema1.model.pk
data['source'] = invalid_id
self.executeBadData(data=data)
data['source'] = self.schema1.pk
data['source'] = self.schema1.model.pk
self.executeOK(data=data)
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
data = {
'receiver': self.schema1.pk,
'source': self.schema2.pk,
'receiver': self.schema1.model.pk,
'source': self.schema2.model.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [
{

View File

@ -6,15 +6,8 @@ from zipfile import ZipFile
from cctext import ReferenceType
from rest_framework import status
from apps.rsform.models import (
AccessPolicy,
Constituenta,
CstType,
LibraryItem,
LibraryItemType,
LocationHead,
RSForm
)
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
@ -24,12 +17,12 @@ class TestRSFormViewset(EndpointTester):
def setUp(self):
super().setUp()
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.pk
self.unowned = RSForm.objects.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.pk
self.private = RSForm.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.pk
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.model.pk
@decl_endpoint('/api/rsforms/create-detailed', method='post')
@ -57,25 +50,25 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms', method='get')
def test_list_rsforms(self):
non_schema = LibraryItem.objects.create(
oss = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATION_SCHEMA,
title='Test3'
)
response = self.executeOK()
self.assertFalse(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned))
self.assertTrue(response_contains(response, self.owned))
self.assertFalse(response_contains(response, oss))
self.assertTrue(response_contains(response, self.unowned.model))
self.assertTrue(response_contains(response, self.owned.model))
@decl_endpoint('/api/rsforms/{item}/contents', method='get')
def test_contents(self):
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible)
self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.model.visible)
@decl_endpoint('/api/rsforms/{item}/details', method='get')
@ -92,12 +85,12 @@ class TestRSFormViewset(EndpointTester):
)
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.visible)
self.assertEqual(response.data['owner'], self.owned.model.owner.pk)
self.assertEqual(response.data['title'], self.owned.model.title)
self.assertEqual(response.data['alias'], self.owned.model.alias)
self.assertEqual(response.data['location'], self.owned.model.location)
self.assertEqual(response.data['access_policy'], self.owned.model.access_policy)
self.assertEqual(response.data['visible'], self.owned.model.visible)
self.assertEqual(len(response.data['items']), 2)
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')
def test_export_trs(self):
schema = RSForm.objects.create(title='Test')
schema = RSForm.create(title='Test')
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')
with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file:
@ -458,7 +451,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/load-trs', method='patch')
def test_load_trs(self):
self.set_params(item=self.owned_id)
self.owned.title = 'Test11'
self.owned.model.title = 'Test11'
self.owned.save()
x1 = self.owned.insert_new('X1')
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')
self.owned.refresh_from_db()
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(self.owned.constituents().count(), 25)
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())

View File

@ -5,22 +5,14 @@ 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('rsforms', views.RSFormViewSet, 'RSForm')
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('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs', views.TrsImportView.as_view()),
path('rsforms/create-detailed', views.create_rsform),
path('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('rslang/parse-expression', views.parse_expression),

View File

@ -1,8 +1,5 @@
''' Utility functions '''
import json
import re
from io import BytesIO
from zipfile import ZipFile
# Name for JSON inside Exteor files archive
EXTEOR_INNER_FILENAME = 'document.json'
@ -11,23 +8,6 @@ EXTEOR_INNER_FILENAME = 'document.json'
_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:
''' Apply mapping to matching in regular expression pattern subgroup 1 '''
if text == '' or pattern == '':

View File

@ -1,8 +1,6 @@
''' REST API: Endpoint processors. '''
from .cctext import generate_lexeme, inflect, parse_text
from .constituents import ConstituentAPIView
from .library import LibraryActiveView, LibraryAdminView, LibraryTemplatesView, LibraryViewSet
from .operations import inline_synthesis
from .rsforms import RSFormViewSet, TrsImportView, create_rsform
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)
schema = cast(m.RSForm, serializer.validated_data['receiver'])
receiver = m.RSForm(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic():
new_items = schema.insert_copy(items)
new_items = receiver.insert_copy(items)
for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution'])
@ -41,10 +41,10 @@ def inline_synthesis(request: Request):
else:
index = next(i for (i, cst) in enumerate(items) if cst == replacement)
replacement = new_items[index]
schema.substitute(original, replacement, substitution['transfer_term'])
schema.restore_order()
receiver.substitute(original, replacement, substitution['transfer_term'])
receiver.restore_order()
return Response(
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.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 permissions
from shared import permissions, utility
from .. import models as m
from .. import serializers as s
@ -25,11 +28,11 @@ from .. import utils
@extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. '''
queryset = m.RSForm.objects.all()
serializer_class = s.LibraryItemSerializer
queryset = LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM)
serializer_class = LibraryItemSerializer
def _get_schema(self) -> m.RSForm:
return cast(m.RSForm, self.get_object())
def _get_item(self) -> LibraryItem:
return cast(LibraryItem, self.get_object())
def get_permissions(self):
''' Determine permission class. '''
@ -71,18 +74,18 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['post'], url_path='create-cst')
def create_cst(self, request: Request, pk):
''' Create new constituenta. '''
schema = self._get_schema()
schema = self._get_item()
serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
if 'insert_after' in data and data['insert_after'] is not None:
try:
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)
else:
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()
response = Response(
@ -109,7 +112,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='produce-structure')
def produce_structure(self, request: Request, pk):
''' 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.is_valid(raise_exception=True)
@ -123,7 +126,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data={f'{cst.id}': msg.constituentaNoStructure()}
)
result = schema.produce_structure(cst, cst_parse)
result = m.RSForm(schema).produce_structure(cst, cst_parse)
return Response(
status=c.HTTP_200_OK,
data={
@ -146,7 +149,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='rename-cst')
def rename_cst(self, request: Request, pk):
''' Rename constituenta possibly changing type. '''
schema = self._get_schema()
schema = self._get_item()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
@ -158,10 +161,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic():
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()
cst.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
@ -184,7 +187,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='substitute')
def substitute(self, request: Request, pk):
''' Substitute occurrences of constituenta with another one. '''
schema = self._get_schema()
schema = self._get_item()
serializer = s.CstSubstituteSerializer(
data=request.data,
context={'schema': schema}
@ -195,7 +198,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original'])
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()
return Response(
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')
def delete_multiple_cst(self, request: Request, pk):
''' Endpoint: Delete multiple constituents. '''
schema = self._get_schema()
schema = self._get_item()
serializer = s.CstListSerializer(
data=request.data,
context={'schema': schema}
)
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()
return Response(
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')
def move_cst(self, request: Request, pk):
''' Endpoint: Move multiple constituents. '''
schema = self._get_schema()
schema = self._get_item()
serializer = s.CstMoveSerializer(
data=request.data,
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
schema.move_cst(
m.RSForm(schema).move_cst(
listCst=serializer.validated_data['items'],
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')
def reset_aliases(self, request: Request, pk):
''' Endpoint: Recreate all aliases based on order. '''
schema = self._get_schema()
schema.reset_aliases()
schema = self._get_item()
m.RSForm(schema).reset_aliases()
return Response(
status=c.HTTP_200_OK,
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')
def restore_order(self, request: Request, pk):
''' Endpoint: Restore order based on types and term graph. '''
schema = self._get_schema()
schema.restore_order()
schema = self._get_item()
m.RSForm(schema).restore_order()
return Response(
status=c.HTTP_200_OK,
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. '''
input_serializer = s.RSFormUploadSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
schema = self._get_schema()
schema = self._get_item()
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
serializer = s.RSFormTRSSerializer(
@ -324,10 +330,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'load_meta': load_metadata}
)
serializer.is_valid(raise_exception=True)
result = serializer.save()
result: m.RSForm = serializer.save()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result).data
data=s.RSFormParseSerializer(result.model).data
)
@extend_schema(
@ -342,10 +348,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='contents')
def contents(self, request: Request, pk):
''' Endpoint: View schema db contents (including constituents). '''
schema = s.RSFormSerializer(self.get_object())
serializer = s.RSFormSerializer(self.get_object())
return Response(
status=c.HTTP_200_OK,
data=schema.data
data=serializer.data
)
@extend_schema(
@ -360,7 +366,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk):
''' Endpoint: Detailed schema view including statuses and parse. '''
serializer = s.RSFormParseSerializer(self._get_schema())
serializer = s.RSFormParseSerializer(self.get_object())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -381,8 +387,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
schema = s.PyConceptAdapter(self._get_schema())
result = pyconcept.check_expression(json.dumps(schema.data), expression)
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
result = pyconcept.check_expression(json.dumps(pySchema.data), expression)
return Response(
status=c.HTTP_200_OK,
data=json.loads(result)
@ -403,7 +409,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text']
resolver = self._get_schema().resolver()
resolver = m.RSForm(self.get_object()).resolver()
resolver.resolve(text)
return Response(
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')
def export_trs(self, request: Request, pk):
''' Endpoint: Download Exteor compatible file. '''
data = s.RSFormTRSSerializer(self._get_schema()).data
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(self._get_schema().alias)
schema = self._get_item()
data = s.RSFormTRSSerializer(m.RSForm(schema)).data
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['Content-Disposition'] = f'attachment; filename={filename}'
return response
@ -440,33 +447,32 @@ class TrsImportView(views.APIView):
tags=['RSForm'],
request=s.FileSerializer,
responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None
}
)
def post(self, request: Request):
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
owner = cast(m.User, self.request.user)
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
owner = cast(User, self.request.user)
_prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(
data=data,
context={'load_meta': True}
)
serializer.is_valid(raise_exception=True)
schema = serializer.save()
result = s.LibraryItemSerializer(schema)
schema: m.RSForm = serializer.save()
return Response(
status=c.HTTP_201_CREATED,
data=result.data
data=LibraryItemSerializer(schema.model).data
)
@extend_schema(
summary='create new RSForm empty or from file',
tags=['RSForm'],
request=s.LibraryItemSerializer,
request=LibraryItemSerializer,
responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None
}
@ -474,26 +480,25 @@ class TrsImportView(views.APIView):
@api_view(['POST'])
def create_rsform(request: Request):
''' 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:
return Response(
status=c.HTTP_400_BAD_REQUEST,
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)
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True)
schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema)
schema: m.RSForm = serializer_rsform.save()
return Response(
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
if 'title' in request.data and 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'
data['read_only'] = read_only
data['access_policy'] = request.data.get('access_policy', m.AccessPolicy.PUBLIC)
data['location'] = request.data.get('location', m.LocationHead.USER)
data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC)
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 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 . import models

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
from rest_framework import status
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

View File

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

View File

@ -1,6 +1,6 @@
''' Utilities for testing. '''
from apps.rsform.models import LibraryItem
from apps.library.models import LibraryItem
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,
ILibraryUpdateData,
ITargetAccessPolicy,
ITargetLocation
ITargetLocation,
IVersionData
} from '@/models/library';
import { IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
import { ITargetUser, ITargetUsers } from '@/models/user';
import {
@ -113,3 +114,10 @@ export function deleteUnsubscribe(target: string, request: FrontAction) {
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.
*/
import { ILibraryCreateData, ILibraryItem, IVersionData } from '@/models/library';
import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import {
IConstituentaList,
@ -13,8 +13,7 @@ import {
IProduceStructureResponse,
IRSFormData,
IRSFormUploadData,
ITargetCst,
IVersionCreatedResponse
ITargetCst
} from '@/models/rsform';
import { IExpressionParse, IRSExpression } from '@/models/rslang';
@ -40,7 +39,7 @@ export function getRSFormDetails(target: string, version: string, request: Front
});
} else {
AxiosGet({
endpoint: `/api/rsforms/${target}/versions/${version}`,
endpoint: `/api/library/${target}/versions/${version}`,
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,
patchSetLocation,
patchSetOwner,
postCreateVersion,
postSubscribe
} from '@/backend/library';
import { patchInlineSynthesis } from '@/backend/operations';
@ -24,8 +25,7 @@ import {
patchRestoreOrder,
patchSubstituteConstituents,
patchUploadTRS,
postCreateConstituenta,
postCreateVersion
postCreateConstituenta
} from '@/backend/rsforms';
import { deleteVersion, patchRestoreVersion, patchVersion } from '@/backend/versions';
import { type ErrorData } from '@/components/info/InfoError';