Compare commits

...

10 Commits

Author SHA1 Message Date
Ivan
2eff1b27b9 F: Improve OSS UI
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-07-26 17:30:37 +03:00
Ivan
3c961c8192 Update README.md 2024-07-26 00:39:45 +03:00
Ivan
583083c1fd F: Improve OSS UI 2024-07-26 00:33:22 +03:00
Ivan
8632848207 npm update 2024-07-25 23:37:00 +03:00
Ivan
a611486c69 F: Fix side animation for LibraryPage 2024-07-25 23:32:09 +03:00
Ivan
ecb26c3908 Add inheritance model 2024-07-25 21:18:44 +03:00
Ivan
90dcf7a8eb R: restructure backend
Warning! This will reset database migrations. Data should be imported manually
2024-07-25 19:12:31 +03:00
Ivan
95caab2919 Refactoring: endpoint naming unifications 2024-07-24 23:45:06 +03:00
Ivan
c438b6ac16 Refactoring: split backendAPI into modules 2024-07-24 23:20:45 +03:00
Ivan
de8e0e60ac Add sync functionality for Operation 2024-07-24 22:23:05 +03:00
130 changed files with 6192 additions and 5667 deletions

View File

@ -11,13 +11,14 @@
[![Frontend CI](https://github.com/IRBorisov/ConceptPortal/actions/workflows/frontend.yml/badge.svg?branch=main)](https://github.com/IRBorisov/ConceptPortal/actions/workflows/frontend.yml)
React + Django based web portal for editing RSForm schemas.
This readme file is used mostly to document project dependencies
This readme file is used mostly to document project dependencies and conventions.
## ❤️ Contributing notes
- feel free to open issues, discussion topics, contact maintainer directly
- use Test config in VSCode to run tests before pushing commits / requests
- use github actions to setup linter checks and test builds
- use conventional commits to describe changes
## ✨ Frontend [Vite + React + Typescript]
@ -138,6 +139,14 @@ This readme file is used mostly to document project dependencies
# Developer Notes
## 📝 Commit conventions
- 🚀 F: major feature implementation
- 💄 D: UI design
- 🚑 B: bug fix
- 🔧 R: refactoring and code improvement
- 📝 I: documentation
## 🖥️ Local build (Windows 10+)
This is the build for local Development

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='Владелец',
@ -128,7 +129,30 @@ class LibraryItem(Model):
@transaction.atomic
def save(self, *args, **kwargs):
subscribe = not self.pk and self.owner
''' Save updating subscriptions and connected operations. '''
if not self._state.adding:
self._update_connected_operations()
subscribe = self._state.adding and self.owner
super().save(*args, **kwargs)
if subscribe:
Subscription.subscribe(user=self.owner, item=self)
def _update_connected_operations(self):
# using method level import to prevent circular dependency
from apps.oss.models import Operation # pylint: disable=import-outside-toplevel
operations = Operation.objects.filter(result__pk=self.pk, sync_text=True)
if not operations.exists():
return
for operation in operations:
changed = False
if operation.alias != self.alias:
operation.alias = self.alias
changed = True
if operation.title != self.title:
operation.title = self.title
changed = True
if operation.comment != self.comment:
operation.comment = self.comment
changed = True
if changed:
operation.save()

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
@ -179,7 +183,7 @@ class TestLibraryViewset(EndpointTester):
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.location, data['location'])
@decl_endpoint('/api/library/{item}/editors-add', method='patch')
@decl_endpoint('/api/library/{item}/add-editor', method='patch')
def test_add_editor(self):
time_update = self.owned.time_update
@ -203,7 +207,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(set(self.owned.editors()), set([self.user, self.user2]))
@decl_endpoint('/api/library/{item}/editors-remove', method='patch')
@decl_endpoint('/api/library/{item}/remove-editor', method='patch')
def test_remove_editor(self):
time_update = self.owned.time_update
@ -230,7 +234,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(self.owned.editors(), [self.user])
@decl_endpoint('/api/library/{item}/editors-set', method='patch')
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
def test_set_editors(self):
time_update = self.owned.time_update
@ -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
@ -49,9 +52,9 @@ class LibraryViewSet(viewsets.ModelViewSet):
'set_owner',
'set_access_policy',
'set_location',
'editors_add',
'editors_remove',
'editors_set'
'add_editor',
'remove_editor',
'set_editors'
]:
access_level = permissions.ItemOwner
elif self.action in [
@ -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(
@ -220,8 +223,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='editors-add')
def editors_add(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='add-editor')
def add_editor(self, request: Request, pk):
''' Endpoint: Add editor for item. '''
item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data)
@ -240,8 +243,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='editors-remove')
def editors_remove(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='remove-editor')
def remove_editor(self, request: Request, pk):
''' Endpoint: Remove editor for item. '''
item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data)
@ -260,8 +263,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='editors-set')
def editors_set(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='set-editors')
def set_editors(self, request: Request, pk):
''' Endpoint: Set list of editors for item. '''
item = self._get_item()
serializer = s.UsersListSerializer(data=request.data)
@ -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

@ -0,0 +1,28 @@
# Generated by Django 5.0.7 on 2024-07-25 18:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0001_initial'),
('rsform', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Inheritance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_child', to='rsform.constituenta', verbose_name='Наследованная конституента')),
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_parent', to='rsform.constituenta', verbose_name='Исходная конституента')),
],
options={
'verbose_name': 'Наследование синтеза',
'verbose_name_plural': 'Отношение наследования конституент',
'unique_together': {('parent', 'child')},
},
),
]

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

@ -0,0 +1,28 @@
''' Models: Synthesis Inheritance. '''
from django.db.models import CASCADE, ForeignKey, Model
class Inheritance(Model):
''' Inheritance links parent and child constituents in synthesis operation.'''
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 = 'Отношение наследования конституент'
unique_together = [['parent', 'child']]
def __str__(self) -> str:
return f'{self.parent} -> {self.child}'

View File

@ -2,6 +2,7 @@
from django.db.models import (
CASCADE,
SET_NULL,
BooleanField,
CharField,
FloatField,
ForeignKey,
@ -21,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'
)
@ -33,11 +34,15 @@ class Operation(Model):
)
result: ForeignKey = ForeignKey(
verbose_name='Связанная КС',
to='rsform.LibraryItem',
to='library.LibraryItem',
null=True,
on_delete=SET_NULL,
related_name='producer'
)
sync_text: BooleanField = BooleanField(
verbose_name='Синхронизация',
default=True
)
alias: CharField = CharField(
verbose_name='Шифр',

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

@ -1,8 +1,8 @@
''' Models: SynthesisSubstitution. '''
''' Models: Synthesis Substitution. '''
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,7 @@
''' Django: Models. '''
from apps.rsform.models import LibraryItem, LibraryItemType
from .Argument import Argument
from .Inheritance import Inheritance
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
@ -41,9 +41,10 @@ class OperationCreateSerializer(serializers.Serializer):
''' serializer metadata. '''
model = Operation
fields = \
'alias', 'operation_type', 'title', \
'alias', 'operation_type', 'title', 'sync_text', \
'comment', 'result', 'position_x', 'position_y'
create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
positions = serializers.ListField(
@ -85,19 +86,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,16 +1,18 @@
''' Testing models: Operation. '''
from django.test import TestCase
from apps.library.models import LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm
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'
)
@ -21,11 +23,54 @@ 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')
self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.comment, '')
self.assertEqual(self.operation.sync_text, True)
self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0)
def test_sync_from_result(self):
schema = RSForm.create(alias=self.operation.alias)
self.operation.result = schema.model
self.operation.save()
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.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.model.alias = 'KS3'
schema.save()
self.operation.refresh_from_db()
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)
self.operation.result = schema
self.operation.save()
schema.alias = 'KS2'
schema.comment = 'Comment'
schema.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)

View File

@ -1,40 +1,40 @@
''' Testing models: SynthesisSubstitution. '''
''' Testing models: Synthesis Substitution. '''
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
class TestSynthesisSubstitution(TestCase):
''' Testing SynthesisSubstitution model. '''
''' 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)
@ -136,6 +137,7 @@ class TestOssViewset(EndpointTester):
'alias': 'Test3',
'title': 'Test title',
'comment': 'Тест кириллицы',
'sync_text': False,
'position_x': 1,
'position_y': 1,
},
@ -149,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'])
@ -158,6 +162,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(new_operation['comment'], data['item_data']['comment'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['sync_text'], data['item_data']['sync_text'])
self.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
@ -193,15 +198,55 @@ 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}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'comment': 'Comment',
'operation_type': OperationType.INPUT
},
'create_schema': True,
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.comment, data['item_data']['comment'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'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.model.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):

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,36 @@ 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'])
data: dict = serializer.validated_data['item_data']
if data['operation_type'] == m.OperationType.INPUT and serializer.validated_data['create_schema']:
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=data['alias'],
title=data['title'],
comment=data['comment'],
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
data['result'] = schema
new_operation = oss.create_operation(**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 +140,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:
@ -186,7 +179,7 @@ class TestRSFormViewset(EndpointTester):
self.assertIn('document.json', zipped_file.namelist())
@decl_endpoint('/api/rsforms/{item}/cst-create', method='post')
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
def test_create_constituenta(self):
data = {'alias': 'X3'}
self.executeForbidden(data=data, item=self.unowned_id)
@ -229,7 +222,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
@decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch')
@decl_endpoint('/api/rsforms/{item}/rename-cst', method='patch')
def test_rename_constituenta(self):
x1 = self.owned.insert_new(
alias='X1',
@ -279,7 +272,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x1.cst_type, CstType.TERM)
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_single(self):
x1 = self.owned.insert_new(
alias='X1',
@ -316,7 +309,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2')
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_multiple(self):
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')
@ -362,7 +355,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(d3.definition_formal, r'D1 \ D2')
@decl_endpoint('/api/rsforms/{item}/cst-create', method='post')
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
def test_create_constituenta_data(self):
data = {
'alias': 'X3',
@ -383,7 +376,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['new_cst']['definition_resolved'], '4')
@decl_endpoint('/api/rsforms/{item}/cst-delete-multiple', method='patch')
@decl_endpoint('/api/rsforms/{item}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self):
self.set_params(item=self.owned_id)
@ -407,7 +400,7 @@ class TestRSFormViewset(EndpointTester):
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/rsforms/{item}/cst-moveto', method='patch')
@decl_endpoint('/api/rsforms/{item}/move-cst', method='patch')
def test_move_constituenta(self):
self.set_params(item=self.owned_id)
@ -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,13 +460,13 @@ 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())
@decl_endpoint('/api/rsforms/{item}/cst-produce-structure', method='patch')
@decl_endpoint('/api/rsforms/{item}/produce-structure', method='patch')
def test_produce_structure(self):
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')

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,21 +28,24 @@ 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. '''
if self.action in [
'load_trs',
'create_cst',
'delete_multiple_cst',
'rename_cst',
'move_cst',
'substitute',
'restore_order',
'reset_aliases',
'cst_create',
'cst_delete_multiple',
'cst_rename',
'cst_substitute'
'produce_structure'
]:
permission_list = [permissions.ItemEditor]
elif self.action in [
@ -65,21 +71,21 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request: Request, pk):
@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(
@ -103,10 +109,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='cst-produce-structure')
@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)
@ -120,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={
@ -140,10 +146,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='cst-rename')
def cst_rename(self, request: Request, pk):
@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)
@ -155,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={
@ -178,10 +184,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='cst-substitute')
def cst_substitute(self, request: Request, pk):
@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}
@ -192,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,
@ -210,16 +217,17 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
def cst_delete_multiple(self, request: Request, pk):
@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,
@ -237,16 +245,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request: Request, pk):
@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']
)
@ -268,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
@ -288,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
@ -311,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(
@ -321,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(
@ -339,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(
@ -357,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
@ -378,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)
@ -400,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,
@ -419,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
@ -437,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
}
@ -471,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']
@ -511,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

File diff suppressed because it is too large Load Diff

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

@ -14,7 +14,7 @@
"@uiw/react-codemirror": "^4.23.0",
"axios": "^1.7.2",
"clsx": "^2.1.1",
"framer-motion": "^11.3.8",
"framer-motion": "^11.3.17",
"html-to-image": "^1.11.11",
"js-file-download": "^0.4.12",
"react": "^18.3.1",
@ -36,24 +36,24 @@
"devDependencies": {
"@lezer/generator": "^1.7.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.11",
"@types/node": "^20.14.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-react-refresh": "^0.4.9",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"ts-jest": "^29.2.3",
"typescript": "^5.5.3",
"vite": "^5.3.4"
"typescript": "^5.5.4",
"vite": "^5.3.5"
}
},
"node_modules/@alloc/quick-lru": {
@ -740,9 +740,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.28.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.6.tgz",
"integrity": "sha512-bhwB1AZ6zU4M3dNKm8Aa2BXwj5mWDqE9IWpqxYKJoLCnx+AcwcMuLO01tLWgc1mx4vT1IVYVqx86YoqUsATrqQ==",
"version": "6.29.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.29.0.tgz",
"integrity": "sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.4.0",
@ -797,14 +797,14 @@
}
},
"node_modules/@emotion/cache": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.12.0.tgz",
"integrity": "sha512-VFo/F1PthkxHwWDCcXkidyXw70eAkdiNiCzthMI2rRQjFiTvmXt8UDlv/VE1DTsd4CIEY2wQf5AnL2QiPgphlw==",
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.3.0",
"@emotion/utils": "^1.3.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
@ -837,17 +837,17 @@
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.12.0.tgz",
"integrity": "sha512-kTktYMpG8mHjLi8u6XOTMfDmQvUve/un2ZVj4khcU2KTn17ElMV8BK6QFzT8V/v2QW8013rf07Yc0ayQL3tp3w==",
"version": "11.13.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz",
"integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/cache": "^11.12.0",
"@emotion/serialize": "^1.2.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.3.0",
"@emotion/cache": "^11.13.0",
"@emotion/serialize": "^1.3.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
@ -861,22 +861,22 @@
}
},
"node_modules/@emotion/serialize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.2.0.tgz",
"integrity": "sha512-X5UWpZAhGGp5LOn7OAI9k9JjRtz7nSFhZypatADcuEd/0bECZ0DzVjPdL8hljTrAku8+TjFvWIYHMOCO/0v/Ng==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz",
"integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.9.0",
"@emotion/utils": "^1.3.0",
"@emotion/utils": "^1.4.0",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.3.0.tgz",
"integrity": "sha512-vOPwbKw8fj/oSEa7CWqiKCvLZ1AeLIAApmboGP34xUyUjXalFyf+tMtgMDqP7VMevLPhUa+YWJS46cQUA+tr9A==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
@ -886,18 +886,18 @@
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
"integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.3.0.tgz",
"integrity": "sha512-+M7u4EaX5t4bCunKTltAdGis3NFHQniikLVEQ+rPQccsX/xV4v5Etwg12paioZ9DsO+CTvimtmnjZbW85kbF8Q==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz",
"integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
@ -1411,28 +1411,28 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
"integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz",
"integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.4"
"@floating-ui/utils": "^0.2.5"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
"integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz",
"integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.4"
"@floating-ui/utils": "^0.2.5"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz",
"integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==",
"license": "MIT"
},
"node_modules/@formatjs/ecma402-abstract": {
@ -2969,9 +2969,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
"integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz",
"integrity": "sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==",
"cpu": [
"arm"
],
@ -2983,9 +2983,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
"integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.0.tgz",
"integrity": "sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==",
"cpu": [
"arm64"
],
@ -2997,9 +2997,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
"integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz",
"integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==",
"cpu": [
"arm64"
],
@ -3011,9 +3011,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
"integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz",
"integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==",
"cpu": [
"x64"
],
@ -3025,9 +3025,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
"integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.0.tgz",
"integrity": "sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==",
"cpu": [
"arm"
],
@ -3039,9 +3039,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
"integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.0.tgz",
"integrity": "sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==",
"cpu": [
"arm"
],
@ -3053,9 +3053,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
"integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.0.tgz",
"integrity": "sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==",
"cpu": [
"arm64"
],
@ -3067,9 +3067,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
"integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.0.tgz",
"integrity": "sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==",
"cpu": [
"arm64"
],
@ -3081,9 +3081,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
"integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.0.tgz",
"integrity": "sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==",
"cpu": [
"ppc64"
],
@ -3095,9 +3095,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
"integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.0.tgz",
"integrity": "sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==",
"cpu": [
"riscv64"
],
@ -3109,9 +3109,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
"integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.0.tgz",
"integrity": "sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==",
"cpu": [
"s390x"
],
@ -3123,9 +3123,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
"integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz",
"integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==",
"cpu": [
"x64"
],
@ -3137,9 +3137,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
"integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.0.tgz",
"integrity": "sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==",
"cpu": [
"x64"
],
@ -3151,9 +3151,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
"integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.0.tgz",
"integrity": "sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==",
"cpu": [
"arm64"
],
@ -3165,9 +3165,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
"integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.0.tgz",
"integrity": "sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==",
"cpu": [
"ia32"
],
@ -3179,9 +3179,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
"integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.0.tgz",
"integrity": "sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==",
"cpu": [
"x64"
],
@ -3253,9 +3253,9 @@
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.2",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz",
"integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==",
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
@ -3634,9 +3634,9 @@
}
},
"node_modules/@types/node": {
"version": "20.14.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
"version": "20.14.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3719,9 +3719,9 @@
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.166.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.166.0.tgz",
"integrity": "sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==",
"version": "0.167.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.167.0.tgz",
"integrity": "sha512-BC+Vbm0d6yMzct7dhTBe9ZjEh6ygupyn1k/UcZncIIS/5aNIbfvF77gQw1IFP09Oyj1UxWj0EUBBqc1GkqzsOw==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -3756,17 +3756,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
"integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz",
"integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/type-utils": "7.16.1",
"@typescript-eslint/utils": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"@typescript-eslint/scope-manager": "7.17.0",
"@typescript-eslint/type-utils": "7.17.0",
"@typescript-eslint/utils": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -3790,16 +3790,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
"integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz",
"integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/typescript-estree": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"@typescript-eslint/scope-manager": "7.17.0",
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/typescript-estree": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0",
"debug": "^4.3.4"
},
"engines": {
@ -3819,14 +3819,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
"integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz",
"integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1"
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -3837,14 +3837,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
"integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz",
"integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.16.1",
"@typescript-eslint/utils": "7.16.1",
"@typescript-eslint/typescript-estree": "7.17.0",
"@typescript-eslint/utils": "7.17.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -3865,9 +3865,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
"integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz",
"integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==",
"dev": true,
"license": "MIT",
"engines": {
@ -3879,14 +3879,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
"integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz",
"integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -3908,16 +3908,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
"integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz",
"integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/typescript-estree": "7.16.1"
"@typescript-eslint/scope-manager": "7.17.0",
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/typescript-estree": "7.17.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -3931,13 +3931,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
"integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz",
"integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/types": "7.17.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -4675,9 +4675,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001642",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true,
"funding": [
{
@ -5449,9 +5449,9 @@
}
},
"node_modules/detect-gpu": {
"version": "5.0.39",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.39.tgz",
"integrity": "sha512-qs+7gnNNxsH4RN1IPpQieU2XNO+RhgemuaRhcawiUug6oXb0Glup90H1YGSjslPO30Sw0E4yfjRoGtSEURwVPQ==",
"version": "5.0.40",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.40.tgz",
"integrity": "sha512-5v4jDN/ERdZZitD29UiLjV9Q9+lDfw2OhEJACIqnvdWulVZCy2K6EwonZ/VKyo4YMqvSIzGIDmojX3jGL3dLpA==",
"license": "MIT",
"dependencies": {
"webgl-constants": "^1.1.1"
@ -5567,9 +5567,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.830",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.830.tgz",
"integrity": "sha512-TrPKKH20HeN0J1LHzsYLs2qwXrp8TF4nHdu4sq61ozGbzMpWhI7iIOPYPPkxeq1azMT9PZ8enPFcftbs/Npcjg==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.1.tgz",
"integrity": "sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w==",
"dev": true,
"license": "ISC"
},
@ -5736,9 +5736,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz",
"integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==",
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz",
"integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -6325,9 +6325,9 @@
}
},
"node_modules/framer-motion": {
"version": "11.3.8",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.8.tgz",
"integrity": "sha512-1D+RDTsIp4Rz2dq/oToqSEc9idEQwgBRQyBq4rGpFba+0Z+GCbj9z1s0+ikFbanWe3YJ0SqkNlDe08GcpFGj5A==",
"version": "11.3.17",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.17.tgz",
"integrity": "sha512-LZcckvZL8Rjod03bud8LQcp+R0PLmWIlOSu+NVc+v6Uh43fQr4IBsEAX7sSn7CdBQ1L0fZ/IqSXZVPnGFSMxHw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@ -6866,9 +6866,9 @@
}
},
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
"integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
"integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9499,9 +9499,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz",
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true,
"license": "MIT"
},
@ -9925,9 +9925,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"dev": true,
"funding": [
{
@ -10041,9 +10041,9 @@
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
"dev": true,
"license": "ISC",
"bin": {
@ -10054,21 +10054,27 @@
}
},
"node_modules/postcss-nested": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.0.11"
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
@ -10776,9 +10782,9 @@
}
},
"node_modules/rollup": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
"integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.0.tgz",
"integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -10792,22 +10798,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.18.1",
"@rollup/rollup-android-arm64": "4.18.1",
"@rollup/rollup-darwin-arm64": "4.18.1",
"@rollup/rollup-darwin-x64": "4.18.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
"@rollup/rollup-linux-arm-musleabihf": "4.18.1",
"@rollup/rollup-linux-arm64-gnu": "4.18.1",
"@rollup/rollup-linux-arm64-musl": "4.18.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
"@rollup/rollup-linux-riscv64-gnu": "4.18.1",
"@rollup/rollup-linux-s390x-gnu": "4.18.1",
"@rollup/rollup-linux-x64-gnu": "4.18.1",
"@rollup/rollup-linux-x64-musl": "4.18.1",
"@rollup/rollup-win32-arm64-msvc": "4.18.1",
"@rollup/rollup-win32-ia32-msvc": "4.18.1",
"@rollup/rollup-win32-x64-msvc": "4.18.1",
"@rollup/rollup-android-arm-eabi": "4.19.0",
"@rollup/rollup-android-arm64": "4.19.0",
"@rollup/rollup-darwin-arm64": "4.19.0",
"@rollup/rollup-darwin-x64": "4.19.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.19.0",
"@rollup/rollup-linux-arm-musleabihf": "4.19.0",
"@rollup/rollup-linux-arm64-gnu": "4.19.0",
"@rollup/rollup-linux-arm64-musl": "4.19.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.19.0",
"@rollup/rollup-linux-riscv64-gnu": "4.19.0",
"@rollup/rollup-linux-s390x-gnu": "4.19.0",
"@rollup/rollup-linux-x64-gnu": "4.19.0",
"@rollup/rollup-linux-x64-musl": "4.19.0",
"@rollup/rollup-win32-arm64-msvc": "4.19.0",
"@rollup/rollup-win32-ia32-msvc": "4.19.0",
"@rollup/rollup-win32-x64-msvc": "4.19.0",
"fsevents": "~2.3.2"
}
},
@ -11346,9 +11352,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -11708,9 +11714,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@ -11849,9 +11855,9 @@
}
},
"node_modules/vite": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -18,7 +18,7 @@
"@uiw/react-codemirror": "^4.23.0",
"axios": "^1.7.2",
"clsx": "^2.1.1",
"framer-motion": "^11.3.8",
"framer-motion": "^11.3.17",
"html-to-image": "^1.11.11",
"js-file-download": "^0.4.12",
"react": "^18.3.1",
@ -40,24 +40,24 @@
"devDependencies": {
"@lezer/generator": "^1.7.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.11",
"@types/node": "^20.14.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-react-refresh": "^0.4.9",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"ts-jest": "^29.2.3",
"typescript": "^5.5.3",
"vite": "^5.3.4"
"typescript": "^5.5.4",
"vite": "^5.3.5"
},
"jest": {
"preset": "ts-jest",

View File

@ -1,577 +0,0 @@
/**
* Module: API for backend communications.
*/
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify';
import { type ErrorData } from '@/components/info/InfoError';
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library';
import { ILibraryCreateData } from '@/models/library';
import {
ICstSubstituteData,
IOperationCreateData,
IOperationCreatedResponse,
IOperationSchemaData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import {
IConstituentaList,
IConstituentaMeta,
ICstCreateData,
ICstCreatedResponse,
ICstMovetoData,
ICstRenameData,
ICstUpdateData,
IInlineSynthesisData,
IProduceStructureResponse,
IRSFormCloneData,
IRSFormData,
IRSFormUploadData,
ITargetCst,
IVersionCreatedResponse
} from '@/models/rsform';
import { IExpressionParse, IRSExpression } from '@/models/rslang';
import {
ICurrentUser,
IPasswordTokenData,
IRequestPasswordData,
IResetPasswordData,
ITargetUser,
ITargetUsers,
IUserInfo,
IUserLoginData,
IUserProfile,
IUserSignupData,
IUserUpdateData,
IUserUpdatePassword
} from '@/models/user';
import { buildConstants } from '@/utils/buildConstants';
const defaultOptions = {
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'x-csrftoken',
baseURL: `${buildConstants.backend}`,
withCredentials: true
};
const axiosInstance = axios.create(defaultOptions);
axiosInstance.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
if (token) {
config.headers['x-csrftoken'] = token;
}
return config;
});
// ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData;
onSuccess?: DataCallback<ResponseData>;
onError?: (error: ErrorData) => void;
setLoading?: (loading: boolean) => void;
showError?: boolean;
}
export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> {
data: DataType;
}
export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType> {
onSuccess: DataCallback<DataType>;
}
export interface FrontExchange<RequestData, ResponseData> extends IFrontRequest<RequestData, ResponseData> {
data: RequestData;
onSuccess: DataCallback<ResponseData>;
}
export interface FrontAction extends IFrontRequest<undefined, undefined> {}
interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string;
request: IFrontRequest<RequestData, ResponseData>;
options?: AxiosRequestConfig;
}
// ==================== API ====================
export function getAuth(request: FrontPull<ICurrentUser>) {
AxiosGet({
endpoint: `/users/api/auth`,
request: request
});
}
export function postLogin(request: FrontPush<IUserLoginData>) {
AxiosPost({
endpoint: '/users/api/login',
request: request
});
}
export function postLogout(request: FrontAction) {
AxiosPost({
endpoint: '/users/api/logout',
request: request
});
}
export function postSignup(request: FrontExchange<IUserSignupData, IUserProfile>) {
AxiosPost({
endpoint: '/users/api/signup',
request: request
});
}
export function getProfile(request: FrontPull<IUserProfile>) {
AxiosGet({
endpoint: '/users/api/profile',
request: request
});
}
export function patchProfile(request: FrontExchange<IUserUpdateData, IUserProfile>) {
AxiosPatch({
endpoint: '/users/api/profile',
request: request
});
}
export function patchPassword(request: FrontPush<IUserUpdatePassword>) {
AxiosPatch({
endpoint: '/users/api/change-password',
request: request
});
}
export function postRequestPasswordReset(request: FrontPush<IRequestPasswordData>) {
// title: 'Request password reset',
AxiosPost({
endpoint: '/users/api/password-reset',
request: request
});
}
export function postValidatePasswordToken(request: FrontPush<IPasswordTokenData>) {
// title: 'Validate password token',
AxiosPost({
endpoint: '/users/api/password-reset/validate',
request: request
});
}
export function postResetPassword(request: FrontPush<IResetPasswordData>) {
// title: 'Reset password',
AxiosPost({
endpoint: '/users/api/password-reset/confirm',
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
// title: 'Active users list',
AxiosGet({
endpoint: '/users/api/active-users',
request: request
});
}
export function getLibrary(request: FrontPull<ILibraryItem[]>) {
// title: 'Available LibraryItems list',
AxiosGet({
endpoint: '/api/library/active',
request: request
});
}
export function getAdminLibrary(request: FrontPull<ILibraryItem[]>) {
// title: 'All LibraryItems list',
AxiosGet({
endpoint: '/api/library/all',
request: request
});
}
export function getTemplates(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/templates',
request: request
});
}
export function postRSFormFromFile(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/rsforms/create-detailed',
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export function postCreateLibraryItem(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/library',
request: request
});
}
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
AxiosPost({
endpoint: `/api/library/${target}/clone`,
request: request
});
}
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/details`,
request: request
});
} else {
AxiosGet({
endpoint: `/api/rsforms/${target}/versions/${version}`,
request: request
});
}
}
export function patchLibraryItem(target: string, request: FrontExchange<ILibraryUpdateData, ILibraryItem>) {
AxiosPatch({
endpoint: `/api/library/${target}`,
request: request
});
}
export function deleteLibraryItem(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}`,
request: request
});
}
export function patchSetOwner(target: string, request: FrontPush<ITargetUser>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-owner`,
request: request
});
}
export function patchSetAccessPolicy(target: string, request: FrontPush<ITargetAccessPolicy>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-access-policy`,
request: request
});
}
export function patchSetLocation(target: string, request: FrontPush<ITargetLocation>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-location`,
request: request
});
}
export function patchEditorsAdd(target: string, request: FrontPush<ITargetUser>) {
AxiosPatch({
endpoint: `/api/library/${target}/editors-add`,
request: request
});
}
export function patchEditorsRemove(target: string, request: FrontPush<ITargetUser>) {
AxiosPatch({
endpoint: `/api/library/${target}/editors-remove`,
request: request
});
}
export function patchEditorsSet(target: string, request: FrontPush<ITargetUsers>) {
AxiosPatch({
endpoint: `/api/library/${target}/editors-set`,
request: request
});
}
export function postSubscribe(target: string, request: FrontAction) {
AxiosPost({
endpoint: `/api/library/${target}/subscribe`,
request: request
});
}
export function deleteUnsubscribe(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}/unsubscribe`,
request: request
});
}
export function getTRSFile(target: string, version: string, request: FrontPull<Blob>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/export-trs`,
request: request,
options: { responseType: 'blob' }
});
} else {
AxiosGet({
endpoint: `/api/versions/${version}/export-file`,
request: request,
options: { responseType: 'blob' }
});
}
}
export function postCreateConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/cst-create`,
request: request
});
}
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/cst-delete-multiple`,
request: request
});
}
export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
endpoint: `/api/constituents/${target}`,
request: request
});
}
export function patchRenameConstituenta(schema: string, request: FrontExchange<ICstRenameData, ICstCreatedResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/cst-rename`,
request: request
});
}
export function patchProduceStructure(schema: string, request: FrontExchange<ITargetCst, IProduceStructureResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/cst-produce-structure`,
request: request
});
}
export function patchSubstituteConstituents(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/cst-substitute`,
request: request
});
}
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/cst-moveto`,
request: request
});
}
export function postCheckExpression(schema: string, request: FrontExchange<IRSExpression, IExpressionParse>) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/check`,
request: request
});
}
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/reset-aliases`,
request: request
});
}
export function patchRestoreOrder(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/restore-order`,
request: request
});
}
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/load-trs`,
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/operations/inline-synthesis`,
request: request
});
}
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
AxiosGet({
endpoint: `/api/oss/${target}/details`,
request: request
});
}
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) {
AxiosPatch({
endpoint: `/api/oss/${schema}/update-positions`,
request: request
});
}
export function postCreateOperation(
schema: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) {
AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`,
request: request
});
}
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${schema}/delete-operation`,
request: request
});
}
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/inflect`,
request: request
});
}
export function postParseText(request: FrontExchange<ITextRequest, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/parse`,
request: request
});
}
export function postGenerateLexeme(request: FrontExchange<ITextRequest, ILexemeData>) {
// title: `Parse text ${request.data.text}`,
AxiosPost({
endpoint: `/api/cctext/generate-lexeme`,
request: request
});
}
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
// title: `Create version for RSForm id=${target}`,
AxiosPost({
endpoint: `/api/rsforms/${target}/versions/create`,
request: request
});
}
export function patchVersion(target: string, request: FrontPush<IVersionData>) {
// title: `Version id=${target}`,
AxiosPatch({
endpoint: `/api/versions/${target}`,
request: request
});
}
export function patchRestoreVersion(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/versions/${target}/restore`,
request: request
});
}
export function deleteVersion(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/versions/${target}`,
request: request
});
}
// ============ Helper functions =============
function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.get<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
function AxiosPost<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.post<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
function AxiosDelete<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.delete<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
function AxiosPatch<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.patch<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
return response.data;
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}

View File

@ -0,0 +1,25 @@
/**
* Module: communication setup.
*/
import axios from 'axios';
import { buildConstants } from '@/utils/buildConstants';
const defaultOptions = {
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'x-csrftoken',
baseURL: `${buildConstants.backend}`,
withCredentials: true
};
export const axiosInstance = axios.create(defaultOptions);
axiosInstance.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
if (token) {
config.headers['x-csrftoken'] = token;
}
return config;
});

View File

@ -0,0 +1,114 @@
/**
* Module: generic API for backend REST communications.
*/
import { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify';
import { ErrorData } from '@/components/info/InfoError';
import { axiosInstance } from './apiConfiguration';
// ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
export interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData;
onSuccess?: DataCallback<ResponseData>;
onError?: (error: ErrorData) => void;
setLoading?: (loading: boolean) => void;
showError?: boolean;
}
export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> {
data: DataType;
}
export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType> {
onSuccess: DataCallback<DataType>;
}
export interface FrontExchange<RequestData, ResponseData> extends IFrontRequest<RequestData, ResponseData> {
data: RequestData;
onSuccess: DataCallback<ResponseData>;
}
export interface FrontAction extends IFrontRequest<undefined, undefined> {}
export interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string;
request: IFrontRequest<RequestData, ResponseData>;
options?: AxiosRequestConfig;
}
// ================ Transport API calls ================
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.get<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
export function AxiosPost<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.post<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
export function AxiosDelete<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.delete<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
export function AxiosPatch<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
axiosInstance
.patch<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
return response.data;
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}

View File

@ -0,0 +1,28 @@
/**
* Endpoints: cctext.
*/
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { AxiosPost, FrontExchange } from './apiTransport';
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/inflect`,
request: request
});
}
export function postParseText(request: FrontExchange<ITextRequest, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/parse`,
request: request
});
}
export function postGenerateLexeme(request: FrontExchange<ITextRequest, ILexemeData>) {
AxiosPost({
endpoint: `/api/cctext/generate-lexeme`,
request: request
});
}

View File

@ -0,0 +1,14 @@
/**
* Endpoints: constituents.
*/
import { IConstituentaMeta, ICstUpdateData } from '@/models/rsform';
import { AxiosPatch, FrontExchange } from './apiTransport';
export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
endpoint: `/api/constituents/${target}`,
request: request
});
}

View File

@ -0,0 +1,123 @@
/**
* Endpoints: library.
*/
import {
ILibraryCreateData,
ILibraryItem,
ILibraryUpdateData,
ITargetAccessPolicy,
ITargetLocation,
IVersionData
} from '@/models/library';
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
import { ITargetUser, ITargetUsers } from '@/models/user';
import {
AxiosDelete,
AxiosGet,
AxiosPatch,
AxiosPost,
FrontAction,
FrontExchange,
FrontPull,
FrontPush
} from './apiTransport';
export function getLibrary(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/active',
request: request
});
}
export function getAdminLibrary(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/all',
request: request
});
}
export function getTemplates(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/templates',
request: request
});
}
export function postCreateLibraryItem(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/library',
request: request
});
}
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
AxiosPost({
endpoint: `/api/library/${target}/clone`,
request: request
});
}
export function patchLibraryItem(target: string, request: FrontExchange<ILibraryUpdateData, ILibraryItem>) {
AxiosPatch({
endpoint: `/api/library/${target}`,
request: request
});
}
export function deleteLibraryItem(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}`,
request: request
});
}
export function patchSetOwner(target: string, request: FrontPush<ITargetUser>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-owner`,
request: request
});
}
export function patchSetAccessPolicy(target: string, request: FrontPush<ITargetAccessPolicy>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-access-policy`,
request: request
});
}
export function patchSetLocation(target: string, request: FrontPush<ITargetLocation>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-location`,
request: request
});
}
export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-editors`,
request: request
});
}
export function postSubscribe(target: string, request: FrontAction) {
AxiosPost({
endpoint: `/api/library/${target}/subscribe`,
request: request
});
}
export function deleteUnsubscribe(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}/unsubscribe`,
request: request
});
}
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
AxiosPost({
endpoint: `/api/library/${target}/create-version`,
request: request
});
}

View File

@ -0,0 +1,14 @@
/**
* Endpoints: operations.
*/
import { IInlineSynthesisData, IRSFormData } from '@/models/rsform';
import { AxiosPatch, FrontExchange } from './apiTransport';
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/operations/inline-synthesis`,
request: request
});
}

View File

@ -0,0 +1,44 @@
/**
* Endpoints: oss.
*/
import {
IOperationCreateData,
IOperationCreatedResponse,
IOperationSchemaData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull, FrontPush } from './apiTransport';
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
AxiosGet({
endpoint: `/api/oss/${target}/details`,
request: request
});
}
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) {
AxiosPatch({
endpoint: `/api/oss/${schema}/update-positions`,
request: request
});
}
export function postCreateOperation(
schema: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) {
AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`,
request: request
});
}
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${schema}/delete-operation`,
request: request
});
}

View File

@ -0,0 +1,137 @@
/**
* Endpoints: rsforms.
*/
import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import {
IConstituentaList,
ICstCreateData,
ICstCreatedResponse,
ICstMovetoData,
ICstRenameData,
IProduceStructureResponse,
IRSFormData,
IRSFormUploadData,
ITargetCst
} from '@/models/rsform';
import { IExpressionParse, IRSExpression } from '@/models/rslang';
import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport';
export function postRSFormFromFile(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/rsforms/create-detailed',
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/details`,
request: request
});
} else {
AxiosGet({
endpoint: `/api/library/${target}/versions/${version}`,
request: request
});
}
}
export function getTRSFile(target: string, version: string, request: FrontPull<Blob>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/export-trs`,
request: request,
options: { responseType: 'blob' }
});
} else {
AxiosGet({
endpoint: `/api/versions/${version}/export-file`,
request: request,
options: { responseType: 'blob' }
});
}
}
export function postCreateConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/create-cst`,
request: request
});
}
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/delete-multiple-cst`,
request: request
});
}
export function patchRenameConstituenta(schema: string, request: FrontExchange<ICstRenameData, ICstCreatedResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/rename-cst`,
request: request
});
}
export function patchProduceStructure(schema: string, request: FrontExchange<ITargetCst, IProduceStructureResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/produce-structure`,
request: request
});
}
export function patchSubstituteConstituents(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/substitute`,
request: request
});
}
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/move-cst`,
request: request
});
}
export function postCheckExpression(schema: string, request: FrontExchange<IRSExpression, IExpressionParse>) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/check`,
request: request
});
}
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/reset-aliases`,
request: request
});
}
export function patchRestoreOrder(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/restore-order`,
request: request
});
}
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/load-trs`,
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}

View File

@ -0,0 +1,99 @@
/**
* Endpoints: users.
*/
import {
ICurrentUser,
IPasswordTokenData,
IRequestPasswordData,
IResetPasswordData,
IUserInfo,
IUserLoginData,
IUserProfile,
IUserSignupData,
IUserUpdateData,
IUserUpdatePassword
} from '@/models/user';
import { AxiosGet, AxiosPatch, AxiosPost, FrontAction, FrontExchange, FrontPull, FrontPush } from './apiTransport';
export function getAuth(request: FrontPull<ICurrentUser>) {
AxiosGet({
endpoint: `/users/api/auth`,
request: request
});
}
export function postLogin(request: FrontPush<IUserLoginData>) {
AxiosPost({
endpoint: '/users/api/login',
request: request
});
}
export function postLogout(request: FrontAction) {
AxiosPost({
endpoint: '/users/api/logout',
request: request
});
}
export function postSignup(request: FrontExchange<IUserSignupData, IUserProfile>) {
AxiosPost({
endpoint: '/users/api/signup',
request: request
});
}
export function getProfile(request: FrontPull<IUserProfile>) {
AxiosGet({
endpoint: '/users/api/profile',
request: request
});
}
export function patchProfile(request: FrontExchange<IUserUpdateData, IUserProfile>) {
AxiosPatch({
endpoint: '/users/api/profile',
request: request
});
}
export function patchPassword(request: FrontPush<IUserUpdatePassword>) {
AxiosPatch({
endpoint: '/users/api/change-password',
request: request
});
}
export function postRequestPasswordReset(request: FrontPush<IRequestPasswordData>) {
// title: 'Request password reset',
AxiosPost({
endpoint: '/users/api/password-reset',
request: request
});
}
export function postValidatePasswordToken(request: FrontPush<IPasswordTokenData>) {
// title: 'Validate password token',
AxiosPost({
endpoint: '/users/api/password-reset/validate',
request: request
});
}
export function postResetPassword(request: FrontPush<IResetPasswordData>) {
// title: 'Reset password',
AxiosPost({
endpoint: '/users/api/password-reset/confirm',
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
// title: 'Active users list',
AxiosGet({
endpoint: '/users/api/active-users',
request: request
});
}

View File

@ -0,0 +1,30 @@
/**
* Endpoints: versions.
*/
import { IVersionData } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { AxiosDelete, AxiosPatch, FrontAction, FrontPull, FrontPush } from './apiTransport';
export function patchVersion(target: string, request: FrontPush<IVersionData>) {
// title: `Version id=${target}`,
AxiosPatch({
endpoint: `/api/versions/${target}`,
request: request
});
}
export function patchRestoreVersion(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/versions/${target}/restore`,
request: request
});
}
export function deleteVersion(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/versions/${target}`,
request: request
});
}

View File

@ -0,0 +1,36 @@
import Tooltip from '@/components/ui/Tooltip';
import { OssNodeInternal } from '@/models/miscellaneous';
import { labelOperationType } from '@/utils/labels';
interface TooltipOperationProps {
node: OssNodeInternal;
anchor: string;
}
function TooltipOperation({ node, anchor }: TooltipOperationProps) {
return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'>
<h2>{node.data.operation.alias}</h2>
<p>
<b>Тип:</b> {labelOperationType(node.data.operation.operation_type)}
</p>
{node.data.operation.title ? (
<p>
<b>Название: </b>
{node.data.operation.title}
</p>
) : null}
{node.data.operation.comment ? (
<p>
<b>Комментарий: </b>
{node.data.operation.comment}
</p>
) : null}
<p>
<b>Положение:</b> [{node.xPos}, {node.yPos}]
</p>
</Tooltip>
);
}
export default TooltipOperation;

Some files were not shown because too many files have changed in this diff Show More