mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add sync functionality for Operation
This commit is contained in:
parent
8376c6bda1
commit
4c413ca0f4
|
@ -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='Связанная КС'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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='Шифр',
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user