Compare commits

..

No commits in common. "2eff1b27b987a9f439b339f4de8dc8b698ecd919" and "338ad2bb98f66b2ca6e1a4f2a99177e907aeb264" have entirely different histories.

130 changed files with 5626 additions and 6151 deletions

View File

@ -11,14 +11,13 @@
[![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 and conventions.
This readme file is used mostly to document project dependencies
## ❤️ 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]
@ -139,14 +138,6 @@ This readme file is used mostly to document project dependencies and conventions
# 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

@ -1,62 +0,0 @@
''' 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

@ -1,8 +0,0 @@
''' 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

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

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

@ -1,14 +0,0 @@
''' 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

@ -1,32 +0,0 @@
''' 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

@ -1,94 +0,0 @@
''' 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

@ -1,8 +0,0 @@
''' 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

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
''' 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

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

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.Substitution, SynthesisSubstitutionAdmin)
admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin)

View File

@ -2,7 +2,7 @@
from django.apps import AppConfig
class OssConfig(AppConfig):
class RsformConfig(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-25 16:06
# Generated by Django 5.0.7 on 2024-07-17 09:51
import django.db.models.deletion
from django.db import migrations, models
@ -9,8 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('library', '0001_initial'),
('rsform', '0001_initial'),
('rsform', '0008_alter_libraryitem_item_type'),
]
operations = [
@ -18,15 +17,17 @@ 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='Тип')),
('sync_text', models.BooleanField(default=True, verbose_name='Синхронизация')),
('operation_type', models.CharField(choices=[
('input', 'Input'), ('synthesis', 'Synthesis')], default='input', max_length=10, 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='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='Связанная КС')),
('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='Связанная КС')),
],
options={
'verbose_name': 'Операция',
@ -34,13 +35,16 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='Substitution',
name='SynthesisSubstitution',
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': 'Отождествление синтеза',
@ -51,8 +55,10 @@ class Migration(migrations.Migration):
name='Argument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='oss.operation', verbose_name='Аргумент')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arguments', to='oss.operation', verbose_name='Операция')),
('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='descendants', to='oss.operation', verbose_name='Аргумент')),
('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='arguments', to='oss.operation', verbose_name='Операция')),
],
options={
'verbose_name': 'Аргумент',

View File

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

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

View File

@ -1,28 +0,0 @@
''' 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,7 +2,6 @@
from django.db.models import (
CASCADE,
SET_NULL,
BooleanField,
CharField,
FloatField,
ForeignKey,
@ -22,7 +21,7 @@ class Operation(Model):
''' Operational schema Unit.'''
oss: ForeignKey = ForeignKey(
verbose_name='Схема синтеза',
to='library.LibraryItem',
to='oss.OperationSchema',
on_delete=CASCADE,
related_name='items'
)
@ -34,15 +33,11 @@ class Operation(Model):
)
result: ForeignKey = ForeignKey(
verbose_name='Связанная КС',
to='library.LibraryItem',
to='rsform.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,53 +3,47 @@ from typing import Optional
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import QuerySet
from django.db.models import Manager, QuerySet
from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import LibraryItem, LibraryItemType
from shared import messages as msg
from .Argument import Argument
from .Operation import Operation
from .Substitution import Substitution
from .SynthesisSubstitution import SynthesisSubstitution
class OperationSchema:
class OperationSchema(LibraryItem):
''' Operations schema API. '''
def __init__(self, model: LibraryItem):
self.model = model
class Meta:
''' Model metadata. '''
proxy = True
@staticmethod
def create(**kwargs) -> 'OperationSchema':
''' Create LibraryItem via OperationSchema. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
return OperationSchema(model)
class InternalManager(Manager):
''' Object manager. '''
@staticmethod
def from_id(pk: int) -> 'OperationSchema':
''' Get LibraryItem by pk. '''
model = LibraryItem.objects.get(pk=pk)
return OperationSchema(model)
def get_queryset(self) -> QuerySet:
return super().get_queryset().filter(item_type=LibraryItemType.OPERATION_SCHEMA)
def save(self, *args, **kwargs):
''' Save wrapper. '''
self.model.save(*args, **kwargs)
def create(self, **kwargs):
kwargs.update({'item_type': LibraryItemType.OPERATION_SCHEMA})
return super().create(**kwargs)
def refresh_from_db(self):
''' Model wrapper. '''
self.model.refresh_from_db()
# Legit overriding object manager
objects = InternalManager() # type: ignore[misc]
def operations(self) -> QuerySet[Operation]:
''' Get QuerySet containing all operations of current OSS. '''
return Operation.objects.filter(oss=self.model)
return Operation.objects.filter(oss=self)
def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.model)
return Argument.objects.filter(operation__oss=self)
def substitutions(self) -> QuerySet[Substitution]:
def substitutions(self) -> QuerySet[SynthesisSubstitution]:
''' Operation substitutions. '''
return Substitution.objects.filter(operation__oss=self.model)
return SynthesisSubstitution.objects.filter(operation__oss=self)
def update_positions(self, data: list[dict]):
''' Update positions. '''
@ -66,7 +60,7 @@ class OperationSchema:
''' 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.model, **kwargs)
result = Operation.objects.create(oss=self, **kwargs)
self.save()
result.refresh_from_db()
return result
@ -115,7 +109,7 @@ class OperationSchema:
return
Argument.objects.filter(operation=target).delete()
Substitution.objects.filter(operation=target).delete()
SynthesisSubstitution.objects.filter(operation=target).delete()
# trigger on_change effects
@ -124,9 +118,9 @@ class OperationSchema:
@transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]):
''' Clear all arguments for operation. '''
Substitution.objects.filter(operation=target).delete()
SynthesisSubstitution.objects.filter(operation=target).delete()
for sub in substitutes:
Substitution.objects.create(
SynthesisSubstitution.objects.create(
operation=target,
original=sub['original'],
substitution=sub['substitution'],

View File

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

View File

@ -1,7 +1,8 @@
''' 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 .Substitution import Substitution
from .SynthesisSubstitution import SynthesisSubstitution

View File

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

View File

@ -8,23 +8,11 @@ class TestArgument(TestCase):
''' Testing Argument model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')
self.oss = OperationSchema.objects.create(alias='T1')
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.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.argument = Argument.objects.create(
operation=self.operation2,
argument=self.operation1

View File

@ -1,18 +1,16 @@
''' 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.create(alias='T1')
self.oss = OperationSchema.objects.create(alias='T1')
self.operation = Operation.objects.create(
oss=self.oss.model,
oss=self.oss,
alias='KS1'
)
@ -23,54 +21,11 @@ class TestOperation(TestCase):
def test_create_default(self):
self.assertEqual(self.operation.oss, self.oss.model)
self.assertEqual(self.operation.oss, self.oss)
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

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

View File

@ -2,9 +2,8 @@
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 RSForm
from apps.rsform.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -13,29 +12,29 @@ class TestOssViewset(EndpointTester):
def setUp(self):
super().setUp()
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
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
def populateData(self):
self.ks1 = RSForm.create(alias='KS1', title='Test1', owner=self.user)
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1')
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
self.ks2 = RSForm.create(alias='KS2', title='Test2', owner=self.user)
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2')
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.model
result=self.ks1
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.model
result=self.ks2
)
self.operation3 = self.owned.create_operation(
alias='3',
@ -54,12 +53,12 @@ class TestOssViewset(EndpointTester):
self.populateData()
response = self.executeOK(item=self.owned_id)
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['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['item_type'], LibraryItemType.OPERATION_SCHEMA)
@ -122,12 +121,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(data=data, item=self.private_id)
self.executeForbidden(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)
@ -137,7 +136,6 @@ class TestOssViewset(EndpointTester):
'alias': 'Test3',
'title': 'Test title',
'comment': 'Тест кириллицы',
'sync_text': False,
'position_x': 1,
'position_y': 1,
},
@ -151,9 +149,7 @@ class TestOssViewset(EndpointTester):
self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
response = self.executeCreated(data=data)
self.assertEqual(len(response.data['oss']['items']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
@ -162,7 +158,6 @@ 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'])
@ -198,55 +193,15 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
'result': self.ks1.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)
self.assertEqual(new_operation['result'], self.ks1.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
oss_router = routers.SimpleRouter(trailing_slash=False)
oss_router.register('oss', views.OssViewSet, 'OSS')
library_router = routers.SimpleRouter(trailing_slash=False)
library_router.register('oss', views.OssViewSet, 'OSS')
urlpatterns = [
path('', include(oss_router.urls)),
path('', include(library_router.urls)),
]

View File

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

View File

@ -10,4 +10,60 @@ 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,8 +1,10 @@
# Generated by Django 5.0.7 on 2024-07-25 16:06
# Generated by Django 4.2.4 on 2023-08-26 10:09
import apps.rsform.models
from django.conf import settings
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -10,10 +12,29 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('library', '0001_initial'),
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', '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=[
@ -24,15 +45,28 @@ 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=list, verbose_name='Словоформы')),
('term_forms', models.JSONField(default=apps.rsform.models._empty_forms, 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='library.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='rsform.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

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

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

@ -0,0 +1,30 @@
# Generated by Django 4.2.10 on 2024-03-03 10:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('rsform', '0003_alter_constituenta_definition_raw_and_more'),
]
operations = [
migrations.CreateModel(
name='Version',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.CharField(max_length=20, verbose_name='Версия')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('data', models.JSONField(verbose_name='Содержание')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Схема')),
],
options={
'verbose_name': 'Версии',
'verbose_name_plural': 'Версия',
'unique_together': {('item', 'version')},
},
),
]

View File

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

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

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

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

@ -0,0 +1,35 @@
# 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,6 +20,10 @@ _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'
@ -36,7 +40,7 @@ class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema. '''
schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to='library.LibraryItem',
to='rsform.RSForm',
on_delete=CASCADE
)
order: PositiveIntegerField = PositiveIntegerField(
@ -72,7 +76,7 @@ class Constituenta(Model):
)
term_forms: JSONField = JSONField(
verbose_name='Словоформы',
default=list
default=_empty_forms
)
definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение',

View File

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

View File

@ -54,8 +54,7 @@ class LibraryItem(Model):
item_type: CharField = CharField(
verbose_name='Тип',
max_length=50,
choices=LibraryItemType.choices,
default=LibraryItemType.RSFORM
choices=LibraryItemType.choices
)
owner: ForeignKey = ForeignKey(
verbose_name='Владелец',
@ -129,30 +128,7 @@ class LibraryItem(Model):
@transaction.atomic
def save(self, *args, **kwargs):
''' Save updating subscriptions and connected operations. '''
if not self._state.adding:
self._update_connected_operations()
subscribe = self._state.adding and self.owner
subscribe = not self.pk 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='library.LibraryItem',
to='rsform.RSForm',
on_delete=CASCADE,
null=True
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,3 +21,9 @@ 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,3 +1,6 @@
''' 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, RSForm
from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm
class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self):
self.schema1 = RSForm.create(title='Test1')
self.schema2 = RSForm.create(title='Test2')
self.schema1 = RSForm.objects.create(title='Test1')
self.schema2 = RSForm.objects.create(title='Test2')
def test_str(self):
testStr = 'X1'
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test')
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test')
self.assertEqual(str(cst), testStr)
def test_url(self):
testStr = 'X1'
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test')
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, 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.model)
Constituenta.objects.create(alias='X1', schema=self.schema1)
def test_order_positive_integer(self):
with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=-1)
Constituenta.objects.create(alias='X1', schema=self.schema1, order=-1)
def test_order_min_value(self):
with self.assertRaises(ValidationError):
cst = Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=0)
cst = Constituenta.objects.create(alias='X1', schema=self.schema1, 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.model,
schema=self.schema1,
order=1
)
self.assertEqual(cst.schema, self.schema1.model)
self.assertEqual(cst.schema, self.schema1)
self.assertEqual(cst.order, 1)
self.assertEqual(cst.alias, 'X1')
self.assertEqual(cst.cst_type, CstType.BASE)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
''' Tests for REST API. '''
from .t_cctext import *
from .t_library 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.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create(
alias='X1',
cst_type=CstType.BASE,
schema=self.rsform_owned.model,
schema=self.rsform_owned,
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.model,
schema=self.rsform_unowned,
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.model,
schema=self.rsform_owned,
order=2,
term_raw='Test3',
term_resolved='Test3',

View File

@ -1,16 +1,16 @@
''' Testing API: Library. '''
from rest_framework import status
from apps.library.models import (
from apps.rsform.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 = LibraryItem.objects.create(
self.owned = RSForm.objects.create(
title='Test',
alias='T1',
owner=self.user
)
self.unowned = LibraryItem.objects.create(
self.unowned = RSForm.objects.create(
title='Test2',
alias='T2'
)
self.common = LibraryItem.objects.create(
self.common = RSForm.objects.create(
title='Test3',
alias='T3',
location=LocationHead.COMMON
@ -44,16 +44,12 @@ class TestLibraryViewset(EndpointTester):
'title': 'Title',
'alias': 'alias',
}
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'])
self.executeBadData(data=data)
data = {
'item_type': LibraryItemType.OPERATION_SCHEMA,
'title': 'Title2',
'alias': 'alias2',
'title': 'Title',
'alias': 'alias',
'access_policy': AccessPolicy.PROTECTED,
'visible': False,
'read_only': True
@ -183,7 +179,7 @@ class TestLibraryViewset(EndpointTester):
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.location, data['location'])
@decl_endpoint('/api/library/{item}/add-editor', method='patch')
@decl_endpoint('/api/library/{item}/editors-add', method='patch')
def test_add_editor(self):
time_update = self.owned.time_update
@ -207,7 +203,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(set(self.owned.editors()), set([self.user, self.user2]))
@decl_endpoint('/api/library/{item}/remove-editor', method='patch')
@decl_endpoint('/api/library/{item}/editors-remove', method='patch')
def test_remove_editor(self):
time_update = self.owned.time_update
@ -234,7 +230,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(self.owned.editors(), [self.user])
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
@decl_endpoint('/api/library/{item}/editors-set', method='patch')
def test_set_editors(self):
time_update = self.owned.time_update
@ -363,13 +359,12 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/clone', method='post')
def test_clone_rsform(self):
schema = RSForm(self.owned)
x12 = schema.insert_new(
x12 = self.owned.insert_new(
alias='X12',
term_raw='человек',
term_resolved='человек'
)
d2 = schema.insert_new(
d2 = self.owned.insert_new(
alias='D2',
term_raw='@{X12|plur}',
term_resolved='люди'

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.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')
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')
def test_inline_synthesis_inputs(self):
invalid_id = 1338
data = {
'receiver': self.unowned.model.pk,
'source': self.schema1.model.pk,
'receiver': self.unowned.pk,
'source': self.schema1.pk,
'items': [],
'substitutions': []
}
@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester):
data['receiver'] = invalid_id
self.executeBadData(data=data)
data['receiver'] = self.schema1.model.pk
data['receiver'] = self.schema1.pk
data['source'] = invalid_id
self.executeBadData(data=data)
data['source'] = self.schema1.model.pk
data['source'] = self.schema1.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.model.pk,
'source': self.schema2.model.pk,
'receiver': self.schema1.pk,
'source': self.schema2.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [
{

View File

@ -6,8 +6,15 @@ from zipfile import ZipFile
from cctext import ReferenceType
from rest_framework import status
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.rsform.models import Constituenta, CstType, RSForm
from apps.rsform.models import (
AccessPolicy,
Constituenta,
CstType,
LibraryItem,
LibraryItemType,
LocationHead,
RSForm
)
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
@ -17,12 +24,12 @@ class TestRSFormViewset(EndpointTester):
def setUp(self):
super().setUp()
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
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
@decl_endpoint('/api/rsforms/create-detailed', method='post')
@ -50,25 +57,25 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms', method='get')
def test_list_rsforms(self):
oss = LibraryItem.objects.create(
non_schema = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATION_SCHEMA,
title='Test3'
)
response = self.executeOK()
self.assertFalse(response_contains(response, oss))
self.assertTrue(response_contains(response, self.unowned.model))
self.assertTrue(response_contains(response, self.owned.model))
self.assertFalse(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned))
self.assertTrue(response_contains(response, self.owned))
@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.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['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)
@decl_endpoint('/api/rsforms/{item}/details', method='get')
@ -85,12 +92,12 @@ class TestRSFormViewset(EndpointTester):
)
response = self.executeOK(item=self.owned_id)
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['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(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.pk)
@ -169,9 +176,9 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
def test_export_trs(self):
schema = RSForm.create(title='Test')
schema = RSForm.objects.create(title='Test')
schema.insert_new('X1')
response = self.executeOK(item=schema.model.pk)
response = self.executeOK(item=schema.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:
@ -179,7 +186,7 @@ class TestRSFormViewset(EndpointTester):
self.assertIn('document.json', zipped_file.namelist())
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
@decl_endpoint('/api/rsforms/{item}/cst-create', method='post')
def test_create_constituenta(self):
data = {'alias': 'X3'}
self.executeForbidden(data=data, item=self.unowned_id)
@ -222,7 +229,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
@decl_endpoint('/api/rsforms/{item}/rename-cst', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch')
def test_rename_constituenta(self):
x1 = self.owned.insert_new(
alias='X1',
@ -272,7 +279,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x1.cst_type, CstType.TERM)
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_single(self):
x1 = self.owned.insert_new(
alias='X1',
@ -309,7 +316,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2')
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_multiple(self):
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')
@ -355,7 +362,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(d3.definition_formal, r'D1 \ D2')
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
@decl_endpoint('/api/rsforms/{item}/cst-create', method='post')
def test_create_constituenta_data(self):
data = {
'alias': 'X3',
@ -376,7 +383,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['new_cst']['definition_resolved'], '4')
@decl_endpoint('/api/rsforms/{item}/delete-multiple-cst', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-delete-multiple', method='patch')
def test_delete_constituenta(self):
self.set_params(item=self.owned_id)
@ -400,7 +407,7 @@ class TestRSFormViewset(EndpointTester):
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/rsforms/{item}/move-cst', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-moveto', method='patch')
def test_move_constituenta(self):
self.set_params(item=self.owned_id)
@ -451,7 +458,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.model.title = 'Test11'
self.owned.title = 'Test11'
self.owned.save()
x1 = self.owned.insert_new('X1')
work_dir = os.path.dirname(os.path.abspath(__file__))
@ -460,13 +467,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.model.title, 'Test11')
self.assertEqual(self.owned.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}/produce-structure', method='patch')
@decl_endpoint('/api/rsforms/{item}/cst-produce-structure', method='patch')
def test_produce_structure(self):
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')

View File

@ -15,72 +15,51 @@ class TestVersionViews(EndpointTester):
def setUp(self):
super().setUp()
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.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.unowned = RSForm.objects.create(title='Test2', alias='T2')
self.x1 = self.owned.insert_new(
alias='X1',
convention='testStart'
)
@decl_endpoint('/api/library/{schema}/create-version', method='post')
@decl_endpoint('/api/rsforms/{schema}/versions/create', 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_id)
self.executeBadData(data=invalid_data, schema=self.owned_id)
self.executeForbidden(data=data, schema=self.unowned.pk)
self.executeBadData(data=invalid_data, schema=self.owned.pk)
response = self.executeCreated(data=data, schema=self.owned_id)
response = self.executeCreated(data=data, schema=self.owned.pk)
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/library/{schema}/versions/{version}', method='get')
@decl_endpoint('/api/rsforms/{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_id, version=invalid_id)
self.executeNotFound(schema=self.owned.pk, version=invalid_id)
self.executeNotFound(schema=invalid_id, version=version_id)
self.executeNotFound(schema=self.unowned_id, version=version_id)
self.executeNotFound(schema=self.unowned.pk, version=version_id)
self.owned.model.alias = 'NewName'
self.owned.alias = 'NewName'
self.owned.save()
self.x1.alias = 'X33'
self.x1.save()
response = self.executeOK(schema=self.owned_id, version=version_id)
self.assertNotEqual(response.data['alias'], self.owned.model.alias)
response = self.executeOK(schema=self.owned.pk, version=version_id)
self.assertNotEqual(response.data['alias'], self.owned.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'}
@ -94,7 +73,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_id)
self.assertEqual(response.data['item'], self.owned.pk)
data = {'version': '1.2.0', 'description': 'test1'}
self.method = 'patch'
@ -116,6 +95,25 @@ 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
@ -125,7 +123,7 @@ class TestVersionViews(EndpointTester):
response = self.executeOK(version=version_id)
self.assertEqual(
response.headers['Content-Disposition'],
f'attachment; filename={self.owned.model.alias}.trs'
f'attachment; filename={self.owned.alias}.trs'
)
with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file:
@ -167,7 +165,7 @@ class TestVersionViews(EndpointTester):
def _create_version(self, data) -> int:
response = self.client.post(
f'/api/library/{self.owned_id}/create-version',
f'/api/rsforms/{self.owned.pk}/versions/create',
data=data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

View File

@ -5,14 +5,22 @@ 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,5 +1,8 @@
''' 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'
@ -8,6 +11,23 @@ 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,6 +1,8 @@
''' 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

@ -13,9 +13,6 @@ 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
@ -52,9 +49,9 @@ class LibraryViewSet(viewsets.ModelViewSet):
'set_owner',
'set_access_policy',
'set_location',
'add_editor',
'remove_editor',
'set_editors'
'editors_add',
'editors_remove',
'editors_set'
]:
access_level = permissions.ItemOwner
elif self.action in [
@ -76,7 +73,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
tags=['Library'],
request=s.LibraryItemCloneSerializer,
responses={
c.HTTP_201_CREATED: RSFormParseSerializer,
c.HTTP_201_CREATED: s.RSFormParseSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
@ -91,7 +88,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
if item.item_type != m.LibraryItemType.RSFORM:
return Response(status=c.HTTP_400_BAD_REQUEST)
clone = deepcopy(item)
schema = m.RSForm.objects.get(pk=item.pk)
clone = deepcopy(schema)
clone.pk = None
clone.owner = self.request.user
clone.title = serializer.validated_data['title']
@ -105,14 +103,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic():
clone.save()
need_filter = 'items' in request.data
for cst in RSForm(item).constituents():
for cst in schema.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=RSFormParseSerializer(clone).data
data=s.RSFormParseSerializer(clone).data
)
@extend_schema(
@ -129,7 +127,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(User, self.request.user), item=item)
m.Subscription.subscribe(user=cast(m.User, self.request.user), item=item)
return Response(status=c.HTTP_200_OK)
@extend_schema(
@ -146,7 +144,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(User, self.request.user), item=item)
m.Subscription.unsubscribe(user=cast(m.User, self.request.user), item=item)
return Response(status=c.HTTP_200_OK)
@extend_schema(
@ -186,8 +184,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
item = self._get_item()
serializer = s.AccessPolicySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
new_policy = serializer.validated_data['access_policy']
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=new_policy)
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=serializer.validated_data['access_policy'])
return Response(status=c.HTTP_200_OK)
@extend_schema(
@ -223,8 +220,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='add-editor')
def add_editor(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='editors-add')
def editors_add(self, request: Request, pk):
''' Endpoint: Add editor for item. '''
item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data)
@ -243,8 +240,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='remove-editor')
def remove_editor(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='editors-remove')
def editors_remove(self, request: Request, pk):
''' Endpoint: Remove editor for item. '''
item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data)
@ -263,8 +260,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='set-editors')
def set_editors(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='editors-set')
def editors_set(self, request: Request, pk):
''' Endpoint: Set list of editors for item. '''
item = self._get_item()
serializer = s.UsersListSerializer(data=request.data)
@ -289,7 +286,7 @@ class LibraryActiveView(generics.ListAPIView):
.filter(is_public) \
.filter(common_location).order_by('-time_update')
else:
user = cast(User, self.request.user)
user = cast(m.User, self.request.user)
# pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter(
(is_public & common_location) |

View File

@ -27,11 +27,11 @@ def inline_synthesis(request: Request):
)
serializer.is_valid(raise_exception=True)
receiver = m.RSForm(serializer.validated_data['receiver'])
schema = cast(m.RSForm, serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic():
new_items = receiver.insert_copy(items)
new_items = schema.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]
receiver.substitute(original, replacement, substitution['transfer_term'])
receiver.restore_order()
schema.substitute(original, replacement, substitution['transfer_term'])
schema.restore_order()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(receiver.model).data
data=s.RSFormParseSerializer(schema).data
)

View File

@ -13,11 +13,8 @@ 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, utility
from shared import permissions
from .. import models as m
from .. import serializers as s
@ -28,24 +25,21 @@ from .. import utils
@extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. '''
queryset = LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM)
serializer_class = LibraryItemSerializer
queryset = m.RSForm.objects.all()
serializer_class = s.LibraryItemSerializer
def _get_item(self) -> LibraryItem:
return cast(LibraryItem, self.get_object())
def _get_schema(self) -> m.RSForm:
return cast(m.RSForm, 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',
'produce_structure'
'cst_create',
'cst_delete_multiple',
'cst_rename',
'cst_substitute'
]:
permission_list = [permissions.ItemEditor]
elif self.action in [
@ -71,21 +65,21 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='create-cst')
def create_cst(self, request: Request, pk):
@action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request: Request, pk):
''' Create new constituenta. '''
schema = self._get_item()
schema = self._get_schema()
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 LibraryItem.DoesNotExist:
except m.LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
else:
insert_after = None
new_cst = m.RSForm(schema).create_cst(data, insert_after)
new_cst = schema.create_cst(data, insert_after)
schema.refresh_from_db()
response = Response(
@ -109,10 +103,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='produce-structure')
@action(detail=True, methods=['patch'], url_path='cst-produce-structure')
def produce_structure(self, request: Request, pk):
''' Produce a term for every element of the target constituenta typification. '''
schema = self._get_item()
schema = self._get_schema()
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
@ -126,7 +120,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data={f'{cst.id}': msg.constituentaNoStructure()}
)
result = m.RSForm(schema).produce_structure(cst, cst_parse)
result = schema.produce_structure(cst, cst_parse)
return Response(
status=c.HTTP_200_OK,
data={
@ -146,10 +140,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='rename-cst')
def rename_cst(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='cst-rename')
def cst_rename(self, request: Request, pk):
''' Rename constituenta possibly changing type. '''
schema = self._get_item()
schema = self._get_schema()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
@ -161,10 +155,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic():
cst.save()
m.RSForm(schema).apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
schema.refresh_from_db()
cst.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
@ -184,10 +178,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='substitute')
def substitute(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='cst-substitute')
def cst_substitute(self, request: Request, pk):
''' Substitute occurrences of constituenta with another one. '''
schema = self._get_item()
schema = self._get_schema()
serializer = s.CstSubstituteSerializer(
data=request.data,
context={'schema': schema}
@ -198,8 +192,7 @@ 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'])
m.RSForm(schema).substitute(original, replacement, substitution['transfer_term'])
schema.substitute(original, replacement, substitution['transfer_term'])
schema.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
@ -217,17 +210,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
def delete_multiple_cst(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
def cst_delete_multiple(self, request: Request, pk):
''' Endpoint: Delete multiple constituents. '''
schema = self._get_item()
schema = self._get_schema()
serializer = s.CstListSerializer(
data=request.data,
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
m.RSForm(schema).delete_cst(serializer.validated_data['items'])
schema.delete_cst(serializer.validated_data['items'])
schema.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
@ -245,16 +237,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='move-cst')
def move_cst(self, request: Request, pk):
@action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request: Request, pk):
''' Endpoint: Move multiple constituents. '''
schema = self._get_item()
schema = self._get_schema()
serializer = s.CstMoveSerializer(
data=request.data,
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
m.RSForm(schema).move_cst(
schema.move_cst(
listCst=serializer.validated_data['items'],
target=serializer.validated_data['move_to']
)
@ -276,8 +268,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_item()
m.RSForm(schema).reset_aliases()
schema = self._get_schema()
schema.reset_aliases()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data
@ -296,8 +288,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_item()
m.RSForm(schema).restore_order()
schema = self._get_schema()
schema.restore_order()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema).data
@ -319,10 +311,9 @@ 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_item()
schema = self._get_schema()
load_metadata = input_serializer.validated_data['load_metadata']
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
data['id'] = schema.pk
serializer = s.RSFormTRSSerializer(
@ -330,10 +321,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'load_meta': load_metadata}
)
serializer.is_valid(raise_exception=True)
result: m.RSForm = serializer.save()
result = serializer.save()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result.model).data
data=s.RSFormParseSerializer(result).data
)
@extend_schema(
@ -348,10 +339,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). '''
serializer = s.RSFormSerializer(self.get_object())
schema = s.RSFormSerializer(self.get_object())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
data=schema.data
)
@extend_schema(
@ -366,7 +357,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_object())
serializer = s.RSFormParseSerializer(self._get_schema())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -387,8 +378,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']
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
result = pyconcept.check_expression(json.dumps(pySchema.data), expression)
schema = s.PyConceptAdapter(self._get_schema())
result = pyconcept.check_expression(json.dumps(schema.data), expression)
return Response(
status=c.HTTP_200_OK,
data=json.loads(result)
@ -409,7 +400,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 = m.RSForm(self.get_object()).resolver()
resolver = self._get_schema().resolver()
resolver.resolve(text)
return Response(
status=c.HTTP_200_OK,
@ -428,10 +419,9 @@ 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. '''
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)
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)
response = HttpResponse(file, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}'
return response
@ -447,32 +437,33 @@ class TrsImportView(views.APIView):
tags=['RSForm'],
request=s.FileSerializer,
responses={
c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None
}
)
def post(self, request: Request):
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
owner = cast(User, self.request.user)
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
owner = cast(m.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: m.RSForm = serializer.save()
schema = serializer.save()
result = s.LibraryItemSerializer(schema)
return Response(
status=c.HTTP_201_CREATED,
data=LibraryItemSerializer(schema.model).data
data=result.data
)
@extend_schema(
summary='create new RSForm empty or from file',
tags=['RSForm'],
request=LibraryItemSerializer,
request=s.LibraryItemSerializer,
responses={
c.HTTP_201_CREATED: LibraryItemSerializer,
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None
}
@ -480,25 +471,26 @@ class TrsImportView(views.APIView):
@api_view(['POST'])
def create_rsform(request: Request):
''' Endpoint: Create RSForm from user input and/or trs file. '''
owner = cast(User, request.user) if not request.user.is_anonymous else None
owner = cast(m.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()}
)
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
else:
data = utils.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: m.RSForm = serializer_rsform.save()
schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema)
return Response(
status=c.HTTP_201_CREATED,
data=LibraryItemSerializer(schema.model).data
data=result.data
)
def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None]):
def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None]):
data['owner'] = owner
if 'title' in request.data and request.data['title'] != '':
data['title'] = request.data['title']
@ -519,5 +511,5 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
read_only = request.data['read_only'] == 'true'
data['read_only'] = read_only
data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC)
data['location'] = request.data.get('location', LocationHead.USER)
data['access_policy'] = request.data.get('access_policy', m.AccessPolicy.PUBLIC)
data['location'] = request.data.get('location', m.LocationHead.USER)

View File

@ -10,13 +10,11 @@ from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.request import Request
from rest_framework.response import Response
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 shared import permissions
from .. import models as m
from .. import serializers as s
from .. import utils
@extend_schema(tags=['Version'])
@ -34,7 +32,7 @@ class VersionViewset(
summary='restore version data into current item',
request=None,
responses={
c.HTTP_200_OK: RSFormParseSerializer,
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
@ -44,10 +42,81 @@ class VersionViewset(
''' Restore version data into current item. '''
version = cast(m.Version, self.get_object())
item = cast(m.LibraryItem, version.item)
RSFormSerializer(item).restore_from_version(version.data)
schema = m.RSForm.objects.get(pk=item.pk)
s.RSFormSerializer(schema).restore_from_version(version.data)
return Response(
status=c.HTTP_200_OK,
data=RSFormParseSerializer(item).data
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
)
@ -67,79 +136,10 @@ 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)
data = RSFormTRSSerializer(version.item).from_versioned_data(version.data)
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
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)
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

@ -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.library.models import Editor, Subscription
from apps.rsform.models import Editor, Subscription
from shared import messages as msg
from . import models

View File

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

View File

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

View File

@ -8,7 +8,6 @@ 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.library.models import Editor, LibraryItem
from apps.rsform.models import Editor, LibraryItem
from apps.users.models import User

View File

@ -11,9 +11,15 @@ 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 Constituenta
from apps.rsform.models import (
AccessPolicy,
Constituenta,
Editor,
LibraryItem,
Subscription,
Version
)
from apps.users.models import User

View File

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

View File

@ -1,21 +0,0 @@
''' 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.17",
"framer-motion": "^11.3.8",
"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.12",
"@types/node": "^20.14.11",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@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.9",
"eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"ts-jest": "^29.2.3",
"typescript": "^5.5.4",
"vite": "^5.3.5"
"typescript": "^5.5.3",
"vite": "^5.3.4"
}
},
"node_modules/@alloc/quick-lru": {
@ -740,9 +740,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.29.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.29.0.tgz",
"integrity": "sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==",
"version": "6.28.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.6.tgz",
"integrity": "sha512-bhwB1AZ6zU4M3dNKm8Aa2BXwj5mWDqE9IWpqxYKJoLCnx+AcwcMuLO01tLWgc1mx4vT1IVYVqx86YoqUsATrqQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.4.0",
@ -797,14 +797,14 @@
}
},
"node_modules/@emotion/cache": {
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.12.0.tgz",
"integrity": "sha512-VFo/F1PthkxHwWDCcXkidyXw70eAkdiNiCzthMI2rRQjFiTvmXt8UDlv/VE1DTsd4CIEY2wQf5AnL2QiPgphlw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.0",
"@emotion/sheet": "^1.3.0",
"@emotion/utils": "^1.3.0",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
@ -837,17 +837,17 @@
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.13.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz",
"integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==",
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.12.0.tgz",
"integrity": "sha512-kTktYMpG8mHjLi8u6XOTMfDmQvUve/un2ZVj4khcU2KTn17ElMV8BK6QFzT8V/v2QW8013rf07Yc0ayQL3tp3w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.12.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/cache": "^11.12.0",
"@emotion/serialize": "^1.2.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.3.0",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
@ -861,22 +861,22 @@
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz",
"integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.2.0.tgz",
"integrity": "sha512-X5UWpZAhGGp5LOn7OAI9k9JjRtz7nSFhZypatADcuEd/0bECZ0DzVjPdL8hljTrAku8+TjFvWIYHMOCO/0v/Ng==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.9.0",
"@emotion/utils": "^1.4.0",
"@emotion/utils": "^1.3.0",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.3.0.tgz",
"integrity": "sha512-vOPwbKw8fj/oSEa7CWqiKCvLZ1AeLIAApmboGP34xUyUjXalFyf+tMtgMDqP7VMevLPhUa+YWJS46cQUA+tr9A==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
@ -886,18 +886,18 @@
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"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==",
"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==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz",
"integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.3.0.tgz",
"integrity": "sha512-+M7u4EaX5t4bCunKTltAdGis3NFHQniikLVEQ+rPQccsX/xV4v5Etwg12paioZ9DsO+CTvimtmnjZbW85kbF8Q==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
@ -1411,28 +1411,28 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz",
"integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==",
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
"integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.5"
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz",
"integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==",
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
"integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.5"
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz",
"integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"license": "MIT"
},
"node_modules/@formatjs/ecma402-abstract": {
@ -2969,9 +2969,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"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==",
"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==",
"cpu": [
"arm"
],
@ -2983,9 +2983,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"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==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
"integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
"cpu": [
"arm64"
],
@ -2997,9 +2997,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz",
"integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==",
"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==",
"cpu": [
"arm64"
],
@ -3011,9 +3011,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz",
"integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==",
"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==",
"cpu": [
"x64"
],
@ -3025,9 +3025,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"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==",
"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==",
"cpu": [
"arm"
],
@ -3039,9 +3039,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"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==",
"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==",
"cpu": [
"arm"
],
@ -3053,9 +3053,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"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==",
"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==",
"cpu": [
"arm64"
],
@ -3067,9 +3067,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"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==",
"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==",
"cpu": [
"arm64"
],
@ -3081,9 +3081,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"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==",
"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==",
"cpu": [
"ppc64"
],
@ -3095,9 +3095,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"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==",
"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==",
"cpu": [
"riscv64"
],
@ -3109,9 +3109,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"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==",
"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==",
"cpu": [
"s390x"
],
@ -3123,9 +3123,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"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==",
"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==",
"cpu": [
"x64"
],
@ -3137,9 +3137,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"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==",
"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==",
"cpu": [
"x64"
],
@ -3151,9 +3151,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"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==",
"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==",
"cpu": [
"arm64"
],
@ -3165,9 +3165,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"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==",
"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==",
"cpu": [
"ia32"
],
@ -3179,9 +3179,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"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==",
"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==",
"cpu": [
"x64"
],
@ -3253,9 +3253,9 @@
}
},
"node_modules/@tweenjs/tween.js": {
"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==",
"version": "23.1.2",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz",
"integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
@ -3634,9 +3634,9 @@
}
},
"node_modules/@types/node": {
"version": "20.14.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
"version": "20.14.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3719,9 +3719,9 @@
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.167.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.167.0.tgz",
"integrity": "sha512-BC+Vbm0d6yMzct7dhTBe9ZjEh6ygupyn1k/UcZncIIS/5aNIbfvF77gQw1IFP09Oyj1UxWj0EUBBqc1GkqzsOw==",
"version": "0.166.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.166.0.tgz",
"integrity": "sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -3756,17 +3756,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz",
"integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@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",
"@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",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -3790,16 +3790,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz",
"integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
"integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@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",
"@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",
"debug": "^4.3.4"
},
"engines": {
@ -3819,14 +3819,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz",
"integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
"integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0"
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -3837,14 +3837,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"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==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
"integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.17.0",
"@typescript-eslint/utils": "7.17.0",
"@typescript-eslint/typescript-estree": "7.16.1",
"@typescript-eslint/utils": "7.16.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -3865,9 +3865,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz",
"integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
"integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -3879,14 +3879,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz",
"integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
"integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/visitor-keys": "7.17.0",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/visitor-keys": "7.16.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -3908,16 +3908,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz",
"integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
"integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.17.0",
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/typescript-estree": "7.17.0"
"@typescript-eslint/scope-manager": "7.16.1",
"@typescript-eslint/types": "7.16.1",
"@typescript-eslint/typescript-estree": "7.16.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -3931,13 +3931,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"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==",
"version": "7.16.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
"integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.17.0",
"@typescript-eslint/types": "7.16.1",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -4675,9 +4675,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"version": "1.0.30001642",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
"dev": true,
"funding": [
{
@ -5449,9 +5449,9 @@
}
},
"node_modules/detect-gpu": {
"version": "5.0.40",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.40.tgz",
"integrity": "sha512-5v4jDN/ERdZZitD29UiLjV9Q9+lDfw2OhEJACIqnvdWulVZCy2K6EwonZ/VKyo4YMqvSIzGIDmojX3jGL3dLpA==",
"version": "5.0.39",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.39.tgz",
"integrity": "sha512-qs+7gnNNxsH4RN1IPpQieU2XNO+RhgemuaRhcawiUug6oXb0Glup90H1YGSjslPO30Sw0E4yfjRoGtSEURwVPQ==",
"license": "MIT",
"dependencies": {
"webgl-constants": "^1.1.1"
@ -5567,9 +5567,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.1.tgz",
"integrity": "sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w==",
"version": "1.4.830",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.830.tgz",
"integrity": "sha512-TrPKKH20HeN0J1LHzsYLs2qwXrp8TF4nHdu4sq61ozGbzMpWhI7iIOPYPPkxeq1azMT9PZ8enPFcftbs/Npcjg==",
"dev": true,
"license": "ISC"
},
@ -5736,9 +5736,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz",
"integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==",
"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==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -6325,9 +6325,9 @@
}
},
"node_modules/framer-motion": {
"version": "11.3.17",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.17.tgz",
"integrity": "sha512-LZcckvZL8Rjod03bud8LQcp+R0PLmWIlOSu+NVc+v6Uh43fQr4IBsEAX7sSn7CdBQ1L0fZ/IqSXZVPnGFSMxHw==",
"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==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@ -6866,9 +6866,9 @@
}
},
"node_modules/import-local": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
"integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
"integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9499,9 +9499,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz",
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
"dev": true,
"license": "MIT"
},
@ -9925,9 +9925,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
@ -10041,9 +10041,9 @@
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
"dev": true,
"license": "ISC",
"bin": {
@ -10054,27 +10054,21 @@
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
"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.1.1"
"postcss-selector-parser": "^6.0.11"
},
"engines": {
"node": ">=12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
@ -10782,9 +10776,9 @@
}
},
"node_modules/rollup": {
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.0.tgz",
"integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
"integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -10798,22 +10792,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@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",
"@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",
"fsevents": "~2.3.2"
}
},
@ -11352,9 +11346,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -11714,9 +11708,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@ -11855,9 +11849,9 @@
}
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
"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.17",
"framer-motion": "^11.3.8",
"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.12",
"@types/node": "^20.14.11",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@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.9",
"eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"ts-jest": "^29.2.3",
"typescript": "^5.5.4",
"vite": "^5.3.5"
"typescript": "^5.5.3",
"vite": "^5.3.4"
},
"jest": {
"preset": "ts-jest",

View File

@ -0,0 +1,577 @@
/**
* 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

@ -1,25 +0,0 @@
/**
* 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

@ -1,114 +0,0 @@
/**
* 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

@ -1,28 +0,0 @@
/**
* 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

@ -1,14 +0,0 @@
/**
* 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

@ -1,123 +0,0 @@
/**
* 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

@ -1,14 +0,0 @@
/**
* 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

@ -1,44 +0,0 @@
/**
* 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

@ -1,137 +0,0 @@
/**
* 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

@ -1,99 +0,0 @@
/**
* 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

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

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