Rafactoring: improving backend

This commit is contained in:
Ivan 2024-07-22 21:20:17 +03:00
parent fbaf17ea58
commit c91ff51afa
41 changed files with 530 additions and 257 deletions

View File

@ -47,6 +47,7 @@ cover/
# Django
rsconcept/frontend/static
rsconcept/frontend/media
visualizeDB.dot
*.log
db.sqlite3
db.sqlite3-journal

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ cover/
*.log
db.sqlite3
db.sqlite3-journal
visualizeDB.dot
# React

17
.vscode/launch.json vendored
View File

@ -5,6 +5,7 @@
"version": "0.2.0",
"configurations": [
{
// Run Frontend + Backend with current Database
"name": "Run",
"type": "PowerShell",
"request": "launch",
@ -12,6 +13,7 @@
"args": []
},
{
// Run Linters
"name": "Lint",
"type": "PowerShell",
"request": "launch",
@ -19,6 +21,7 @@
"args": []
},
{
// Run Tests
"name": "Test",
"type": "PowerShell",
"request": "launch",
@ -26,6 +29,7 @@
"args": []
},
{
// Run Tests for backend for current file in Debug mode
"name": "BE-DebugTestFile",
"type": "debugpy",
"request": "launch",
@ -35,6 +39,7 @@
"django": true
},
{
// Run Tests for frontned in Debug mode
"name": "FE-DebugTestAll",
"type": "node",
"request": "launch",
@ -55,6 +60,7 @@
}
},
{
// Run Browser in Debug mode (Backend should be running)
"name": "FE-Debug",
"type": "chrome",
"request": "launch",
@ -62,6 +68,7 @@
"webRoot": "${workspaceFolder}/rsconcept/frontend"
},
{
// Run Backend in Debug mode
"name": "BE-Debug",
"type": "debugpy",
"request": "launch",
@ -70,6 +77,7 @@
"django": true
},
{
// Run Backend test coverage
"name": "BE-Coverage",
"type": "PowerShell",
"request": "launch",
@ -77,11 +85,20 @@
"args": []
},
{
// Recreate database, fill with initial data and Run Backend + Frontend
"name": "Restart",
"type": "PowerShell",
"request": "launch",
"script": "${workspaceFolder}/scripts/dev/RunServer.ps1",
"args": ["-freshStart"]
},
{
// Create DOT file for visualizing database
"name": "BE-GraphDB",
"type": "PowerShell",
"request": "launch",
"script": "${workspaceFolder}/scripts/dev/GraphDB.ps1",
"args": []
}
]
}

View File

@ -7,8 +7,24 @@ from . import models
class OperationAdmin(admin.ModelAdmin):
''' Admin model: Operation. '''
ordering = ['oss']
list_display = ['oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
search_fields = ['operation_type', 'title', 'alias']
list_display = ['id', 'oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
search_fields = ['id', 'operation_type', 'title', 'alias']
class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. '''
ordering = ['operation']
list_display = ['id', 'operation', 'argument']
search_fields = ['id', 'operation', 'argument']
class SynthesisSubstitutionAdmin(admin.ModelAdmin):
''' Admin model: Substitutions as part of Synthesis operation. '''
ordering = ['operation']
list_display = ['id', 'operation', 'original', 'substitution', 'transfer_term']
search_fields = ['id', 'operation', 'original', 'substitution']
admin.site.register(models.Operation, OperationAdmin)
admin.site.register(models.Argument, ArgumentAdmin)
admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin)

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

@ -24,4 +24,4 @@ class Argument(Model):
unique_together = [['operation', 'argument']]
def __str__(self) -> str:
return f'{self.argument.pk} -> {self.operation.pk}'
return f'{self.argument} -> {self.operation}'

View File

@ -21,7 +21,7 @@ class Operation(Model):
''' Operational schema Unit.'''
oss: ForeignKey = ForeignKey(
verbose_name='Схема синтеза',
to='rsform.LibraryItem',
to='oss.OperationSchema',
on_delete=CASCADE,
related_name='items'
)

View File

@ -3,7 +3,7 @@ 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.rsform.models import LibraryItem, LibraryItemType
from shared import messages as msg
@ -13,30 +13,37 @@ from .Operation import Operation
from .SynthesisSubstitution import SynthesisSubstitution
class OperationSchema:
class OperationSchema(LibraryItem):
''' Operations schema API. '''
def __init__(self, item: LibraryItem):
if item.item_type != LibraryItemType.OPERATION_SCHEMA:
raise ValueError(msg.libraryTypeUnexpected())
self.item = item
class Meta:
''' Model metadata. '''
proxy = True
@staticmethod
def create(**kwargs) -> 'OperationSchema':
item = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
return OperationSchema(item=item)
class InternalManager(Manager):
''' Object manager. '''
def get_queryset(self) -> QuerySet:
return super().get_queryset().filter(item_type=LibraryItemType.OPERATION_SCHEMA)
def create(self, **kwargs):
kwargs.update({'item_type': LibraryItemType.OPERATION_SCHEMA})
return super().create(**kwargs)
# 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.item)
return Operation.objects.filter(oss=self)
def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.item)
return Argument.objects.filter(operation__oss=self)
def substitutions(self) -> QuerySet[SynthesisSubstitution]:
''' Operation substitutions. '''
return SynthesisSubstitution.objects.filter(operation__oss=self.item)
return SynthesisSubstitution.objects.filter(operation__oss=self)
def update_positions(self, data: list[dict]):
''' Update positions. '''
@ -53,11 +60,8 @@ 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.item,
**kwargs
)
self.item.save()
result = Operation.objects.create(oss=self, **kwargs)
self.save()
result.refresh_from_db()
return result
@ -69,7 +73,7 @@ class OperationSchema:
# deal with attached schema
# trigger on_change effects
self.item.save()
self.save()
@transaction.atomic
def set_input(self, target: Operation, schema: Optional[LibraryItem]):
@ -87,7 +91,7 @@ class OperationSchema:
# trigger on_change effects
self.item.save()
self.save()
@transaction.atomic
def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]:
@ -95,7 +99,7 @@ class OperationSchema:
if Argument.objects.filter(operation=operation, argument=argument).exists():
return None
result = Argument.objects.create(operation=operation, argument=argument)
self.item.save()
self.save()
return result
@transaction.atomic
@ -109,7 +113,7 @@ class OperationSchema:
# trigger on_change effects
self.item.save()
self.save()
@transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]):
@ -125,4 +129,4 @@ class OperationSchema:
# trigger on_change effects
self.item.save()
self.save()

View File

@ -33,4 +33,4 @@ class SynthesisSubstitution(Model):
verbose_name_plural = 'Таблицы отождествлений'
def __str__(self) -> str:
return f'{self.original.pk} -> {self.substitution.pk}'
return f'{self.original} -> {self.substitution}'

View File

@ -2,7 +2,7 @@
from apps.rsform.models import LibraryItem, LibraryItemType
from .api_OSS import OperationSchema
from .Argument import Argument
from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .SynthesisSubstitution import SynthesisSubstitution

View File

@ -85,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 +1,4 @@
''' Tests for Django Models. '''
from .t_Argument import *
from .t_Operation import *
from .t_SynthesisSubstitution import *

View File

@ -0,0 +1,36 @@
''' Testing models: Argument. '''
from django.test import TestCase
from apps.oss.models import Argument, Operation, OperationSchema, OperationType
class TestArgument(TestCase):
''' Testing Argument model. '''
def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1')
self.operation1 = Operation.objects.create(oss=self.oss, alias='KS1', operation_type=OperationType.INPUT)
self.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS)
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT)
self.argument = Argument.objects.create(
operation=self.operation2,
argument=self.operation1
)
def test_str(self):
testStr = f'{self.operation1} -> {self.operation2}'
self.assertEqual(str(self.argument), testStr)
def test_cascade_delete_operation(self):
self.assertEqual(Argument.objects.count(), 1)
self.operation2.delete()
self.assertEqual(Argument.objects.count(), 0)
def test_cascade_delete_argument(self):
self.assertEqual(Argument.objects.count(), 1)
self.operation1.delete()
self.assertEqual(Argument.objects.count(), 0)

View File

@ -0,0 +1,31 @@
''' Testing models: Operation. '''
from django.test import TestCase
from apps.oss.models import Operation, OperationSchema, OperationType
class TestOperation(TestCase):
''' Testing Operation model. '''
def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1')
self.operation = Operation.objects.create(
oss=self.oss,
alias='KS1'
)
def test_str(self):
testStr = 'Операция KS1'
self.assertEqual(str(self.operation), testStr)
def test_create_default(self):
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.position_x, 0)
self.assertEqual(self.operation.position_y, 0)

View File

@ -0,0 +1,75 @@
''' Testing models: SynthesisSubstitution. '''
from unittest import result
from django.test import TestCase
from apps.oss.models import (
Argument,
Operation,
OperationSchema,
OperationType,
SynthesisSubstitution
)
from apps.rsform.models import RSForm
class TestSynthesisSubstitution(TestCase):
''' Testing SynthesisSubstitution model. '''
def setUp(self):
self.oss = OperationSchema.objects.create(alias='T1')
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1')
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2')
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
self.operation1 = Operation.objects.create(
oss=self.oss,
alias='KS1',
operation_type=OperationType.INPUT,
result=self.ks1)
self.operation2 = Operation.objects.create(
oss=self.oss,
alias='KS2',
operation_type=OperationType.INPUT,
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
)
Argument.objects.create(
operation=self.operation3,
argument=self.operation2
)
self.substitution = SynthesisSubstitution.objects.create(
operation=self.operation3,
original=self.ks1x1,
substitution=self.ks2x1,
transfer_term=False
)
def test_str(self):
testStr = f'{self.ks1x1} -> {self.ks2x1}'
self.assertEqual(str(self.substitution), testStr)
def test_cascade_delete_operation(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.operation3.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
def test_cascade_delete_original(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.ks1x1.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
def test_cascade_delete_substitution(self):
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
self.ks2x1.delete()
self.assertEqual(SynthesisSubstitution.objects.count(), 0)

View File

@ -12,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.item.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.item.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.item.pk
self.invalid_id = self.private.item.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')
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 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1.item
result=self.ks1
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.item
result=self.ks2
)
self.operation3 = self.owned.create_operation(
alias='3',
@ -53,12 +53,12 @@ class TestOssViewset(EndpointTester):
self.populateData()
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title)
self.assertEqual(response.data['alias'], self.owned.item.alias)
self.assertEqual(response.data['location'], self.owned.item.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.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)
@ -179,7 +179,7 @@ class TestOssViewset(EndpointTester):
'arguments': [self.operation1.pk, self.operation3.pk]
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.item.refresh_from_db()
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
arguments = self.owned.arguments()
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
@ -193,14 +193,14 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.item.pk
'result': self.ks1.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.item.refresh_from_db()
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.item.pk)
self.assertEqual(new_operation['result'], self.ks1.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')

View File

@ -20,11 +20,11 @@ from .. import serializers as s
@extend_schema_view()
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: OperationSchema. '''
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.OPERATION_SCHEMA)
queryset = m.OperationSchema.objects.all()
serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.OperationSchema:
return m.OperationSchema(cast(m.LibraryItem, self.get_object()))
return cast(m.OperationSchema, self.get_object())
def get_permissions(self):
''' Determine permission class. '''
@ -52,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(cast(m.LibraryItem, self.get_object()))
serializer = s.OperationSchemaSerializer(self._get_schema())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -101,13 +101,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']:
schema.add_argument(operation=new_operation, argument=argument)
schema.item.refresh_from_db()
schema.refresh_from_db()
response = Response(
status=c.HTTP_201_CREATED,
data={
'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(schema.item).data
'oss': s.OperationSchemaSerializer(schema).data
}
)
return response
@ -129,16 +129,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
schema = self._get_schema()
serializer = s.OperationDeleteSerializer(
data=request.data,
context={'oss': schema.item}
context={'oss': schema}
)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
schema.update_positions(serializer.validated_data['positions'])
schema.delete_operation(serializer.validated_data['target'])
schema.item.refresh_from_db()
schema.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(schema.item).data
data=s.OperationSchemaSerializer(schema).data
)

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

@ -40,7 +40,7 @@ class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema. '''
schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to='rsform.LibraryItem',
to='rsform.RSForm',
on_delete=CASCADE
)
order: PositiveIntegerField = PositiveIntegerField(

View File

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

View File

@ -5,7 +5,7 @@ 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 shared import messages as msg
@ -28,21 +28,29 @@ from .Version import Version
_INSERT_LAST: int = -1
class RSForm:
class RSForm(LibraryItem):
''' RSForm is math form of conceptual schema. '''
def __init__(self, item: LibraryItem):
if item.item_type != LibraryItemType.RSFORM:
raise ValueError(msg.libraryTypeUnexpected())
self.item = item
class Meta:
''' Model metadata. '''
proxy = True
@staticmethod
def create(**kwargs) -> 'RSForm':
return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs))
class InternalManager(Manager):
''' Object manager. '''
def get_queryset(self) -> QuerySet:
return super().get_queryset().filter(item_type=LibraryItemType.RSFORM)
def create(self, **kwargs):
kwargs.update({'item_type': LibraryItemType.RSFORM})
return super().create(**kwargs)
# 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.item)
return Constituenta.objects.filter(schema=self.pk)
def resolver(self) -> Resolver:
''' Create resolver for text references based on schema terms. '''
@ -98,7 +106,7 @@ class RSForm:
''' Get maximum alias index for specific CstType. '''
result: int = 0
items = Constituenta.objects \
.filter(schema=self.item, cst_type=cst_type) \
.filter(schema=self, cst_type=cst_type) \
.order_by('-alias') \
.values_list('alias', flat=True)
for alias in items:
@ -150,13 +158,13 @@ class RSForm:
cst_type = guess_type(alias)
self._shift_positions(position, 1)
result = Constituenta.objects.create(
schema=self.item,
schema=self,
order=position,
alias=alias,
cst_type=cst_type,
**kwargs
)
self.item.save()
self.save()
result.refresh_from_db()
return result
@ -183,13 +191,13 @@ class RSForm:
result = deepcopy(items)
for cst in result:
cst.pk = None
cst.schema = self.item
cst.schema = self
cst.order = position
cst.alias = mapping[cst.alias]
cst.apply_mapping(mapping)
cst.save()
position = position + 1
self.item.save()
self.save()
return result
@transaction.atomic
@ -213,7 +221,7 @@ class RSForm:
count_moved += 1
update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['order'])
self.item.save()
self.save()
@transaction.atomic
def delete_cst(self, listCst):
@ -222,7 +230,7 @@ class RSForm:
cst.delete()
self._reset_order()
self.resolve_all_text()
self.item.save()
self.save()
@transaction.atomic
def substitute(
@ -296,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.item,
item=self,
version=version,
description=description,
data=data
@ -322,7 +330,7 @@ class RSForm:
prefix = get_type_prefix(cst_type)
for text in expressions:
new_item = Constituenta.objects.create(
schema=self.item,
schema=self,
order=position,
alias=f'{prefix}{free_index}',
definition_formal=text,
@ -332,7 +340,7 @@ class RSForm:
free_index = free_index + 1
position = position + 1
self.item.save()
self.save()
return result
def _shift_positions(self, start: int, shift: int):
@ -341,7 +349,7 @@ class RSForm:
update_list = \
Constituenta.objects \
.only('id', 'order', 'schema') \
.filter(schema=self.item, 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

@ -1,6 +1,6 @@
''' Django: Models. '''
from .api_RSForm import RSForm
from .RSForm import RSForm
from .Constituenta import Constituenta, CstType, _empty_forms
from .Editor import Editor
from .LibraryItem import (

View File

@ -109,22 +109,21 @@ class CstSerializer(serializers.ModelSerializer):
def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: use alias for better code readability
schema = RSForm(instance.schema)
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
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.item.save()
instance.schema.save()
return result
@ -170,17 +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
schema = RSForm(instance)
result['items'] = []
for cst in schema.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']
@ -197,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] = []
@ -258,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)
@ -283,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({
@ -315,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({
@ -326,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)
})
@ -338,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
@ -370,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'])
@ -397,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()
@ -406,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

@ -39,9 +39,9 @@ class RSFormTRSSerializer(serializers.Serializer):
def _prepare_json_rsform(schema: RSForm) -> dict:
return {
'type': _TRS_TYPE,
'title': schema.item.title,
'alias': schema.item.alias,
'comment': schema.item.comment,
'title': schema.title,
'alias': schema.alias,
'comment': schema.comment,
'items': [],
'claimed': False,
'selection': [],
@ -125,7 +125,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['comment'] = data.get('comment', '')
if 'id' in data:
result['id'] = data['id']
self.instance = RSForm(LibraryItem.objects.get(pk=result['id']))
self.instance = RSForm.objects.get(pk=result['id'])
return result
def validate(self, attrs: dict):
@ -139,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'],
@ -149,12 +149,12 @@ class RSFormTRSSerializer(serializers.Serializer):
access_policy=validated_data['access_policy'],
location=validated_data['location']
)
self.instance.item.save()
self.instance.save()
order = 1
for cst_data in validated_data['items']:
cst = Constituenta(
alias=cst_data['alias'],
schema=self.instance.item,
schema=self.instance,
order=order,
cst_type=cst_data['cstType'],
)
@ -167,11 +167,11 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic
def update(self, instance: RSForm, validated_data) -> RSForm:
if 'alias' in validated_data:
instance.item.alias = validated_data['alias']
instance.alias = validated_data['alias']
if 'title' in validated_data:
instance.item.title = validated_data['title']
instance.title = validated_data['title']
if 'comment' in validated_data:
instance.item.comment = validated_data['comment']
instance.comment = validated_data['comment']
order = 1
prev_constituents = instance.constituents()
@ -188,7 +188,7 @@ class RSFormTRSSerializer(serializers.Serializer):
else:
cst = Constituenta(
alias=cst_data['alias'],
schema=instance.item,
schema=instance,
order=order,
cst_type=cst_data['cstType'],
)
@ -202,7 +202,7 @@ class RSFormTRSSerializer(serializers.Serializer):
prev_cst.delete()
instance.resolve_all_text()
instance.item.save()
instance.save()
return instance
@staticmethod

View File

@ -3,15 +3,15 @@ from django.db.utils import IntegrityError
from django.forms import ValidationError
from django.test import TestCase
from apps.rsform.models import Constituenta, CstType, LibraryItem, LibraryItemType
from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm
class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self):
self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1')
self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2')
self.schema1 = RSForm.objects.create(title='Test1')
self.schema2 = RSForm.objects.create(title='Test2')
def test_str(self):

View File

@ -1,7 +1,7 @@
''' Testing models: Editor. '''
from django.test import TestCase
from apps.rsform.models import Editor, LibraryItem, LibraryItemType, User
from apps.rsform.models import Editor, LibraryItemType, RSForm, User
class TestEditor(TestCase):
@ -10,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

@ -11,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.item, order=1)
Constituenta.objects.create(alias='X2', schema=schema1.item, 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.item, order=1)
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.item, 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.item)
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.item)
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.item)
self.assertEqual(x3.schema, schema)
self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2)
@ -62,7 +62,7 @@ class TestRSForm(TestCase):
x2.refresh_from_db()
x1.refresh_from_db()
self.assertEqual(x4.order, 3)
self.assertEqual(x4.schema, schema.item)
self.assertEqual(x4.schema, schema)
self.assertEqual(x3.order, 4)
self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2)
@ -94,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.item)
self.assertEqual(x1.schema, self.schema)
x2 = self.schema.insert_new('X2')
self.assertEqual(x2.order, 2)
self.assertEqual(x2.schema, self.schema.item)
self.assertEqual(x2.schema, self.schema)
self.assertEqual(x1.order, 1)
def test_create_cst(self):

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.item,
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.item,
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.item,
schema=self.rsform_owned,
order=2,
term_raw='Test3',
term_resolved='Test3',

View File

@ -20,20 +20,16 @@ class TestLibraryViewset(EndpointTester):
def setUp(self):
super().setUp()
self.owned = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
self.owned = RSForm.objects.create(
title='Test',
alias='T1',
owner=self.user
)
self.schema = RSForm(self.owned)
self.unowned = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
self.unowned = RSForm.objects.create(
title='Test2',
alias='T2'
)
self.common = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
self.common = RSForm.objects.create(
title='Test3',
alias='T3',
location=LocationHead.COMMON
@ -363,12 +359,12 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/clone', method='post')
def test_clone_rsform(self):
x12 = self.schema.insert_new(
x12 = self.owned.insert_new(
alias='X12',
term_raw='человек',
term_resolved='человек'
)
d2 = self.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.item.pk,
'source': self.schema1.item.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.item.pk
data['receiver'] = self.schema1.pk
data['source'] = invalid_id
self.executeBadData(data=data)
data['source'] = self.schema1.item.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.item.pk,
'source': self.schema2.item.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

@ -24,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.item.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.item.pk
self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.item.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')
@ -63,19 +63,19 @@ class TestRSFormViewset(EndpointTester):
)
response = self.executeOK()
self.assertFalse(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned.item))
self.assertTrue(response_contains(response, self.owned.item))
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.item.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title)
self.assertEqual(response.data['alias'], self.owned.item.alias)
self.assertEqual(response.data['location'], self.owned.item.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.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')
@ -92,12 +92,12 @@ class TestRSFormViewset(EndpointTester):
)
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title)
self.assertEqual(response.data['alias'], self.owned.item.alias)
self.assertEqual(response.data['location'], self.owned.item.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.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)
@ -176,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.item.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:
@ -387,7 +387,7 @@ class TestRSFormViewset(EndpointTester):
data = {'items': [x1.pk]}
response = self.executeOK(data=data)
x2.refresh_from_db()
self.owned.item.refresh_from_db()
self.owned.refresh_from_db()
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(self.owned.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2')
@ -449,16 +449,16 @@ 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.item.title = 'Test11'
self.owned.item.save()
self.owned.title = 'Test11'
self.owned.save()
x1 = self.owned.insert_new('X1')
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False}
response = self.client.patch(self.endpoint, data=data, format='multipart')
self.owned.item.refresh_from_db()
self.owned.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.owned.item.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())

View File

@ -15,10 +15,9 @@ class TestVersionViews(EndpointTester):
def setUp(self):
super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user).item
self.schema = RSForm(self.owned)
self.unowned = RSForm.create(title='Test2', alias='T2').item
self.x1 = self.schema.insert_new(
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'
)
@ -135,14 +134,14 @@ class TestVersionViews(EndpointTester):
@decl_endpoint('/api/versions/{version}/restore', method='patch')
def test_restore_version(self):
x1 = self.x1
x2 = self.schema.insert_new('X2')
d1 = self.schema.insert_new('D1', term_raw='TestTerm')
x2 = self.owned.insert_new('X2')
d1 = self.owned.insert_new('D1', term_raw='TestTerm')
data = {'version': '1.0.0', 'description': 'test'}
version_id = self._create_version(data=data)
invalid_id = version_id + 1337
d1.delete()
x3 = self.schema.insert_new('X3')
x3 = self.owned.insert_new('X3')
x1.order = x3.order
x1.convention = 'Test2'
x1.term_raw = 'Test'

View File

@ -85,7 +85,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer = s.LibraryItemCloneSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
item = self._get_item()
clone = deepcopy(item)
if item.item_type != m.LibraryItemType.RSFORM:
return Response(status=c.HTTP_400_BAD_REQUEST)
schema = m.RSForm.objects.get(pk=item.pk)
clone = deepcopy(schema)
clone.pk = None
clone.owner = self.request.user
clone.title = serializer.validated_data['title']
@ -98,9 +102,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic():
clone.save()
if clone.item_type == m.LibraryItemType.RSFORM:
need_filter = 'items' in request.data
for cst in m.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
@ -109,7 +112,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
status=c.HTTP_201_CREATED,
data=s.RSFormParseSerializer(clone).data
)
return Response(status=c.HTTP_400_BAD_REQUEST)
@extend_schema(
summary='subscribe to item',

View File

@ -27,7 +27,7 @@ def inline_synthesis(request: Request):
)
serializer.is_valid(raise_exception=True)
schema = 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():
@ -46,5 +46,5 @@ def inline_synthesis(request: Request):
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)

View File

@ -25,11 +25,11 @@ from .. import utils
@extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. '''
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM)
queryset = m.RSForm.objects.all()
serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.RSForm:
return m.RSForm(cast(m.LibraryItem, self.get_object()))
return cast(m.RSForm, self.get_object())
def get_permissions(self):
''' Determine permission class. '''
@ -81,12 +81,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
insert_after = None
new_cst = schema.create_cst(data, insert_after)
schema.item.refresh_from_db()
schema.refresh_from_db()
response = Response(
status=c.HTTP_201_CREATED,
data={
'new_cst': s.CstSerializer(new_cst).data,
'schema': s.RSFormParseSerializer(schema.item).data
'schema': s.RSFormParseSerializer(schema).data
}
)
response['Location'] = new_cst.get_absolute_url()
@ -108,11 +108,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
''' Produce a term for every element of the target constituenta typification. '''
schema = self._get_schema()
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema.item})
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target'])
schema_details = s.RSFormParseSerializer(schema.item).data['items']
schema_details = s.RSFormParseSerializer(schema).data['items']
cst_parse = next(item for item in schema_details if item['id'] == cst.id)['parse']
if not cst_parse['typification']:
return Response(
@ -125,7 +125,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
status=c.HTTP_200_OK,
data={
'cst_list': result,
'schema': s.RSFormParseSerializer(schema.item).data
'schema': s.RSFormParseSerializer(schema).data
}
)
@ -144,7 +144,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def cst_rename(self, request: Request, pk):
''' Rename constituenta possibly changing type. '''
schema = self._get_schema()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema.item})
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target'])
@ -156,14 +156,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic():
cst.save()
schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
schema.item.refresh_from_db()
schema.refresh_from_db()
cst.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
'new_cst': s.CstSerializer(cst).data,
'schema': s.RSFormParseSerializer(schema.item).data
'schema': s.RSFormParseSerializer(schema).data
}
)
@ -184,7 +184,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema()
serializer = s.CstSubstituteSerializer(
data=request.data,
context={'schema': schema.item}
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
@ -193,10 +193,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution'])
schema.substitute(original, replacement, substitution['transfer_term'])
schema.item.refresh_from_db()
schema.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)
@extend_schema(
@ -216,14 +216,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema()
serializer = s.CstListSerializer(
data=request.data,
context={'schema': schema.item}
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
schema.delete_cst(serializer.validated_data['items'])
schema.item.refresh_from_db()
schema.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)
@extend_schema(
@ -243,7 +243,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema()
serializer = s.CstMoveSerializer(
data=request.data,
context={'schema': schema.item}
context={'schema': schema}
)
serializer.is_valid(raise_exception=True)
schema.move_cst(
@ -252,7 +252,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)
@extend_schema(
@ -272,7 +272,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.reset_aliases()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)
@extend_schema(
@ -292,7 +292,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.restore_order()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
data=s.RSFormParseSerializer(schema).data
)
@extend_schema(
@ -314,7 +314,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema()
load_metadata = input_serializer.validated_data['load_metadata']
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
data['id'] = schema.item.pk
data['id'] = schema.pk
serializer = s.RSFormTRSSerializer(
data=data,
@ -324,7 +324,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
result = serializer.save()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result.item).data
data=s.RSFormParseSerializer(result).data
)
@extend_schema(
@ -357,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(cast(m.LibraryItem, self.get_object()))
serializer = s.RSFormParseSerializer(self._get_schema())
return Response(
status=c.HTTP_200_OK,
data=serializer.data
@ -421,7 +421,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
''' Endpoint: Download Exteor compatible file. '''
data = s.RSFormTRSSerializer(self._get_schema()).data
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(self._get_schema().item.alias)
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
@ -451,7 +451,7 @@ class TrsImportView(views.APIView):
)
serializer.is_valid(raise_exception=True)
schema = serializer.save()
result = s.LibraryItemSerializer(schema.item)
result = s.LibraryItemSerializer(schema)
return Response(
status=c.HTTP_201_CREATED,
data=result.data
@ -483,7 +483,7 @@ def create_rsform(request: Request):
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True)
schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema.item)
result = s.LibraryItemSerializer(schema)
return Response(
status=c.HTTP_201_CREATED,
data=result.data

View File

@ -42,10 +42,11 @@ class VersionViewset(
''' Restore version data into current item. '''
version = cast(m.Version, self.get_object())
item = cast(m.LibraryItem, version.item)
s.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=s.RSFormParseSerializer(item).data
data=s.RSFormParseSerializer(schema).data
)
@ -65,7 +66,7 @@ class VersionViewset(
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)
item = m.RSForm.objects.get(pk=pk_item)
except m.LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND)
creator = request.user
@ -75,7 +76,7 @@ def create_version(request: Request, pk_item: int):
version_input = s.VersionCreateSerializer(data=request.data)
version_input.is_valid(raise_exception=True)
data = s.RSFormSerializer(item).to_versioned_data()
result = m.RSForm(item).create_version(
result = item.create_version(
version=version_input.validated_data['version'],
description=version_input.validated_data['description'],
data=data
@ -102,8 +103,8 @@ def create_version(request: Request, pk_item: int):
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:
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)
@ -135,7 +136,8 @@ 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 = s.RSFormTRSSerializer(m.RSForm(version.item)).from_versioned_data(version.data)
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')

View File

@ -79,6 +79,8 @@ INSTALLED_APPS = [
'drf_spectacular',
'drf_spectacular_sidecar',
]
if DEBUG:
INSTALLED_APPS.append('django_extensions')
REST_FRAMEWORK = {
@ -128,7 +130,6 @@ APPEND_SLASH = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
STATIC_URL = 'static/'
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
@ -156,7 +157,6 @@ WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
@ -198,7 +198,6 @@ SPECTACULAR_SETTINGS = {
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS: list[str] = [
# NOTE: Password validators disabled
# {
@ -231,6 +230,14 @@ USE_TZ = True
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Graph model settings for visualization
# https://django-extensions.readthedocs.io/en/latest/graph_models.html
GRAPH_MODELS = {
'all_applications': True,
'group_models': True,
}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,

View File

@ -14,6 +14,7 @@ psycopg2-binary
gunicorn
djangorestframework-stubs[compatible-mypy]
django-extensions
mypy
pylint
coverage

View File

@ -50,10 +50,6 @@ def typificationInvalidStr():
return 'Invalid typification string'
def libraryTypeUnexpected():
return 'Attempting to use invalid adaptor for non-RSForm item'
def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'

16
scripts/dev/GraphDB.ps1 Normal file
View File

@ -0,0 +1,16 @@
# Generate DOT file for DB structure
$backend = Resolve-Path -Path "${PSScriptRoot}\..\..\rsconcept\backend"
function GenerateDOT() {
Set-Location $backend
$python = "${backend}\venv\Scripts\python.exe"
$djangoSrc = "${backend}\manage.py"
& $python $djangoSrc graph_models -o visualizeDB.dot
notepad.exe "${backend}\visualizeDB.dot"
Start-Process "https://dreampuf.github.io/GraphvizOnline"
}
GenerateDOT

View File

@ -2,7 +2,7 @@
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
function RunLinters() {
function RunCoverage() {
BackendCoverage
}
@ -20,4 +20,4 @@ function BackendCoverage() {
Start-Process "$backend\htmlcov\index.html"
}
RunLinters
RunCoverage