Add sync functionality for Operation

This commit is contained in:
Ivan 2024-07-24 22:23:35 +03:00
parent 8376c6bda1
commit 4c413ca0f4
9 changed files with 152 additions and 17 deletions

View File

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

View File

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

View File

@ -41,7 +41,7 @@ class OperationCreateSerializer(serializers.Serializer):
''' serializer metadata. ''' ''' serializer metadata. '''
model = Operation model = Operation
fields = \ fields = \
'alias', 'operation_type', 'title', \ 'alias', 'operation_type', 'title', 'sync_text', \
'comment', 'result', 'position_x', 'position_y' 'comment', 'result', 'position_x', 'position_y'
item_data = OperationData() item_data = OperationData()

View File

@ -1,7 +1,8 @@
''' Testing models: Operation. ''' ''' Testing models: Operation. '''
from django.test import TestCase from django.test import TestCase
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import LibraryItem, LibraryItemType, Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm
class TestOperation(TestCase): class TestOperation(TestCase):
@ -27,5 +28,48 @@ class TestOperation(TestCase):
self.assertEqual(self.operation.alias, 'KS1') self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '') self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.comment, '') self.assertEqual(self.operation.comment, '')
self.assertEqual(self.operation.sync_text, True)
self.assertEqual(self.operation.position_x, 0) self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0) self.assertEqual(self.operation.position_y, 0)
def test_sync_from_result(self):
schema = RSForm.objects.create(alias=self.operation.alias)
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)
self.operation.sync_text = False
self.operation.save()
schema.alias = 'KS3'
schema.save()
self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema)
self.assertNotEqual(self.operation.alias, schema.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

@ -136,6 +136,7 @@ class TestOssViewset(EndpointTester):
'alias': 'Test3', 'alias': 'Test3',
'title': 'Test title', 'title': 'Test title',
'comment': 'Тест кириллицы', 'comment': 'Тест кириллицы',
'sync_text': False,
'position_x': 1, 'position_x': 1,
'position_y': 1, 'position_y': 1,
}, },
@ -158,6 +159,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(new_operation['comment'], data['item_data']['comment']) 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_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y']) 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.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x']) self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])

View File

@ -128,7 +128,30 @@ class LibraryItem(Model):
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
subscribe = not self.pk and self.owner ''' Save updating subscriptions and connected operations. '''
if not self._state.adding:
self._update_connected_operations()
subscribe = self._state.adding and self.owner
super().save(*args, **kwargs) super().save(*args, **kwargs)
if subscribe: if subscribe:
Subscription.subscribe(user=self.owner, item=self) 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

@ -40,6 +40,7 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [inputs, setInputs] = useState<OperationID[]>([]); const [inputs, setInputs] = useState<OperationID[]>([]);
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined); const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [syncText, setSyncText] = useState(true);
const isValid = useMemo(() => alias !== '', [alias]); const isValid = useMemo(() => alias !== '', [alias]);
@ -62,6 +63,7 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
alias: alias, alias: alias,
title: title, title: title,
comment: comment, comment: comment,
sync_text: activeTab === TabID.INPUT ? syncText : true,
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS, operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
result: activeTab === TabID.INPUT ? attachedID ?? null : null result: activeTab === TabID.INPUT ? attachedID ?? null : null
}, },
@ -83,10 +85,12 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
setTitle={setTitle} setTitle={setTitle}
attachedID={attachedID} attachedID={attachedID}
setAttachedID={setAttachedID} setAttachedID={setAttachedID}
syncText={syncText}
setSyncText={setSyncText}
/> />
</TabPanel> </TabPanel>
), ),
[alias, comment, title, attachedID] [alias, comment, title, attachedID, syncText]
); );
const synthesisPanel = useMemo( const synthesisPanel = useMemo(

View File

@ -1,5 +1,9 @@
import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import Checkbox from '@/components/ui/Checkbox';
import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
@ -15,6 +19,8 @@ interface TabInputOperationProps {
setComment: React.Dispatch<React.SetStateAction<string>>; setComment: React.Dispatch<React.SetStateAction<string>>;
attachedID: LibraryItemID | undefined; attachedID: LibraryItemID | undefined;
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>; setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
syncText: boolean;
setSyncText: React.Dispatch<React.SetStateAction<boolean>>;
} }
function TabInputOperation({ function TabInputOperation({
@ -25,7 +31,9 @@ function TabInputOperation({
comment, comment,
setComment, setComment,
attachedID, attachedID,
setAttachedID setAttachedID,
syncText,
setSyncText
}: TabInputOperationProps) { }: TabInputOperationProps) {
return ( return (
<AnimateFade className='cc-column'> <AnimateFade className='cc-column'>
@ -34,17 +42,27 @@ function TabInputOperation({
label='Полное название' label='Полное название'
value={title} value={title}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
disabled={syncText && attachedID !== undefined}
/> />
<div className='flex gap-6'> <div className='flex gap-6'>
<TextInput <FlexColumn>
id='operation_alias' <TextInput
label='Сокращение' id='operation_alias'
className='w-[14rem]' label='Сокращение'
pattern={patterns.library_alias} className='w-[14rem]'
title={`не более ${limits.library_alias_len} символов`} pattern={patterns.library_alias}
value={alias} title={`не более ${limits.library_alias_len} символов`}
onChange={event => setAlias(event.target.value)} value={alias}
/> onChange={event => setAlias(event.target.value)}
disabled={syncText && attachedID !== undefined}
/>
<Checkbox
value={syncText}
setValue={setSyncText}
label='Синхронизировать текст'
title='Брать текст из концептуальной схемы'
/>
</FlexColumn>
<TextArea <TextArea
id='operation_comment' id='operation_comment'
@ -53,10 +71,22 @@ function TabInputOperation({
rows={3} rows={3}
value={comment} value={comment}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
disabled={syncText && attachedID !== undefined}
/>
</div>
<div className='flex gap-3 items-center'>
<Label text='Загружаемая концептуальная схема' />
<MiniButton
title='Сбросить выбор схемы'
noHover
noPadding
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => setAttachedID(undefined)}
disabled={attachedID == undefined}
/> />
</div> </div>
<Label text='Загружаемая концептуальная схема' />
<PickSchema value={attachedID} onSelectValue={setAttachedID} rows={8} /> <PickSchema value={attachedID} onSelectValue={setAttachedID} rows={8} />
</AnimateFade> </AnimateFade>
); );

View File

@ -30,6 +30,8 @@ export interface IOperation {
alias: string; alias: string;
title: string; title: string;
comment: string; comment: string;
sync_text: boolean;
position_x: number; position_x: number;
position_y: number; position_y: number;
@ -61,7 +63,7 @@ export interface ITargetOperation extends IPositionsData {
export interface IOperationCreateData extends IPositionsData { export interface IOperationCreateData extends IPositionsData {
item_data: Pick< item_data: Pick<
IOperation, IOperation,
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' 'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' | 'sync_text'
>; >;
arguments: OperationID[] | undefined; arguments: OperationID[] | undefined;
} }