F: Add ordering to OSS arguments

This commit is contained in:
Ivan 2024-09-04 14:35:17 +03:00
parent 0dda271014
commit 318fadcc5f
20 changed files with 983 additions and 916 deletions

View File

@ -14,7 +14,7 @@ class OperationAdmin(admin.ModelAdmin):
class ArgumentAdmin(admin.ModelAdmin): class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. ''' ''' Admin model: Operation arguments. '''
ordering = ['operation'] ordering = ['operation']
list_display = ['id', 'operation', 'argument'] list_display = ['id', 'order', 'operation', 'argument']
search_fields = ['id', 'operation', 'argument'] search_fields = ['id', 'operation', 'argument']

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2024-09-04 09:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0006_alter_operation_oss'),
]
operations = [
migrations.AddField(
model_name='argument',
name='order',
field=models.PositiveIntegerField(default=0, verbose_name='Позиция'),
),
]

View File

@ -1,5 +1,5 @@
''' Models: Operation Argument in OSS. ''' ''' Models: Operation Argument in OSS. '''
from django.db.models import CASCADE, ForeignKey, Model from django.db.models import CASCADE, ForeignKey, Model, PositiveIntegerField
class Argument(Model): class Argument(Model):
@ -16,6 +16,10 @@ class Argument(Model):
on_delete=CASCADE, on_delete=CASCADE,
related_name='descendants' related_name='descendants'
) )
order: PositiveIntegerField = PositiveIntegerField(
verbose_name='Позиция',
default=0,
)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''

View File

@ -137,23 +137,27 @@ class OperationSchema:
self.cache.ensure_loaded() self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target] operation = self.cache.operation_by_id[target]
processed: list[Operation] = [] processed: list[Operation] = []
updated: list[Argument] = []
deleted: list[Argument] = [] deleted: list[Argument] = []
for current in operation.getArguments(): for current in operation.getArguments():
if current.argument not in arguments: if current.argument not in arguments:
deleted.append(current) deleted.append(current)
else: else:
processed.append(current.argument) processed.append(current.argument)
current.order = arguments.index(current.argument)
updated.append(current)
if len(deleted) > 0: if len(deleted) > 0:
self.before_delete_arguments(operation, [x.argument for x in deleted]) self.before_delete_arguments(operation, [x.argument for x in deleted])
for deleted_arg in deleted: for deleted_arg in deleted:
self.cache.remove_argument(deleted_arg) self.cache.remove_argument(deleted_arg)
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete() Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
Argument.objects.bulk_update(updated, ['order'])
added: list[Operation] = [] added: list[Operation] = []
for arg in arguments: for order, arg in enumerate(arguments):
if arg not in processed: if arg not in processed:
processed.append(arg) processed.append(arg)
new_arg = Argument.objects.create(operation=operation, argument=arg) new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
self.cache.insert_argument(new_arg) self.cache.insert_argument(new_arg)
added.append(arg) added.append(arg)
if len(added) > 0: if len(added) > 0:
@ -219,7 +223,7 @@ class OperationSchema:
def execute_operation(self, operation: Operation) -> bool: def execute_operation(self, operation: Operation) -> bool:
''' Execute target operation. ''' ''' Execute target operation. '''
schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments().order_by('pk')] schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments().order_by('order')]
if None in schemas: if None in schemas:
return False return False
substitutions = operation.getSubstitutions() substitutions = operation.getSubstitutions()
@ -693,7 +697,7 @@ class OssCache:
self.graph = Graph[int]() self.graph = Graph[int]()
for operation in self.operations: for operation in self.operations:
self.graph.add_node(operation.pk) self.graph.add_node(operation.pk)
for argument in self._oss.arguments().only('operation_id', 'argument_id'): for argument in self._oss.arguments().only('operation_id', 'argument_id').order_by('order'):
self.graph.add_edge(argument.argument_id, argument.operation_id) self.graph.add_edge(argument.argument_id, argument.operation_id)
self.is_loaded = False self.is_loaded = False

View File

@ -210,7 +210,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
for operation in oss.operations().order_by('pk'): for operation in oss.operations().order_by('pk'):
result['items'].append(OperationSerializer(operation).data) result['items'].append(OperationSerializer(operation).data)
result['arguments'] = [] result['arguments'] = []
for argument in oss.arguments().order_by('pk'): for argument in oss.arguments().order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = [] result['substitutions'] = []
for substitution in oss.substitutions().values( for substitution in oss.substitutions().values(

View File

@ -1,4 +1,5 @@
''' Testing models: Argument. ''' ''' Testing models: Argument. '''
from django.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
from apps.oss.models import Argument, Operation, OperationSchema, OperationType from apps.oss.models import Argument, Operation, OperationSchema, OperationType
@ -36,6 +37,15 @@ class TestArgument(TestCase):
self.assertEqual(str(self.argument), testStr) self.assertEqual(str(self.argument), testStr)
def test_order_positive_integer(self):
with self.assertRaises(IntegrityError):
Argument.objects.create(
operation=self.operation2,
argument=self.operation3,
order=-1
)
def test_cascade_delete_operation(self): def test_cascade_delete_operation(self):
self.assertEqual(Argument.objects.count(), 1) self.assertEqual(Argument.objects.count(), 1)
self.operation2.delete() self.operation2.delete()

View File

@ -387,7 +387,7 @@ class TestOssViewset(EndpointTester):
'comment': 'Comment mod' 'comment': 'Comment mod'
}, },
'positions': [], 'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk], 'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [ 'substitutions': [
{ {
'original': self.ks1X1.pk, 'original': self.ks1X1.pk,
@ -409,7 +409,11 @@ class TestOssViewset(EndpointTester):
self.assertEqual(self.operation3.alias, data['item_data']['alias']) self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title']) self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.comment, data['item_data']['comment']) self.assertEqual(self.operation3.comment, data['item_data']['comment'])
self.assertEqual(set([argument.pk for argument in self.operation3.getArguments()]), set(data['arguments'])) args = self.operation3.getArguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
self.assertEqual(args[1].argument.pk, data['arguments'][1])
self.assertEqual(args[1].order, 1)
sub = self.operation3.getSubstitutions()[0] sub = self.operation3.getSubstitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original']) self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution']) self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-09-04 09:56
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='constituenta',
name='order',
field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция'),
),
]

View File

@ -58,7 +58,7 @@ class Constituenta(Model):
order: PositiveIntegerField = PositiveIntegerField( order: PositiveIntegerField = PositiveIntegerField(
verbose_name='Позиция', verbose_name='Позиция',
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
default=-1, default=1,
) )
alias: CharField = CharField( alias: CharField = CharField(
verbose_name='Имя', verbose_name='Имя',

View File

@ -20,11 +20,6 @@ class TestConstituenta(TestCase):
self.assertEqual(str(cst), testStr) self.assertEqual(str(cst), testStr)
def test_order_not_null(self):
with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1.model)
def test_order_positive_integer(self): def test_order_positive_integer(self):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=-1) Constituenta.objects.create(alias='X1', schema=self.schema1.model, order=-1)

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -9,7 +9,7 @@ interface BadgeWordFormProps {
function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) { function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) {
return ( return (
<div className='flex flex-wrap justify-start gap-1 select-none'> <div className='flex flex-wrap justify-start gap-1 select-none w-fit'>
{form.grams.map(gram => ( {form.grams.map(gram => (
<BadgeGrammeme key={`${keyPrefix}-${gram}`} grammeme={gram} /> <BadgeGrammeme key={`${keyPrefix}-${gram}`} grammeme={gram} />
))} ))}

View File

@ -2,7 +2,7 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { IconRemove } from '@/components/Icons'; import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/Icons';
import SelectOperation from '@/components/select/SelectOperation'; import SelectOperation from '@/components/select/SelectOperation';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -20,7 +20,10 @@ interface PickMultiOperationProps {
const columnHelper = createColumnHelper<IOperation>(); const columnHelper = createColumnHelper<IOperation>();
function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOperationProps) { function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOperationProps) {
const selectedItems = useMemo(() => items.filter(item => selected.includes(item.id)), [items, selected]); const selectedItems = useMemo(
() => selected.map(itemID => items.find(item => item.id === itemID)!),
[items, selected]
);
const nonSelectedItems = useMemo(() => items.filter(item => !selected.includes(item.id)), [items, selected]); const nonSelectedItems = useMemo(() => items.filter(item => !selected.includes(item.id)), [items, selected]);
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined); const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
@ -40,6 +43,36 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
[setSelected] [setSelected]
); );
const handleMoveUp = useCallback(
(operation: OperationID) => {
const index = selected.indexOf(operation);
if (index > 0) {
setSelected(prev => {
const newSelected = [...prev];
newSelected[index] = newSelected[index - 1];
newSelected[index - 1] = operation;
return newSelected;
});
}
},
[setSelected, selected]
);
const handleMoveDown = useCallback(
(operation: OperationID) => {
const index = selected.indexOf(operation);
if (index < selected.length - 1) {
setSelected(prev => {
const newSelected = [...prev];
newSelected[index] = newSelected[index + 1];
newSelected[index + 1] = operation;
return newSelected;
});
}
},
[setSelected, selected]
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
@ -59,17 +92,35 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
size: 0,
cell: props => ( cell: props => (
<MiniButton <div className='flex gap-1 w-fit'>
noHover <MiniButton
title='Удалить' noHover
icon={<IconRemove size='1rem' className='icon-red' />} className='px-0'
onClick={() => handleDelete(props.row.original.id)} title='Удалить'
/> icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDelete(props.row.original.id)}
/>
<MiniButton
noHover
className='px-0'
title='Выше'
icon={<IconMoveUp size='1rem' className='icon-primary' />}
onClick={() => handleMoveUp(props.row.original.id)}
/>
<MiniButton
noHover
title='Ниже'
className='px-0'
icon={<IconMoveDown size='1rem' className='icon-primary' />}
onClick={() => handleMoveDown(props.row.original.id)}
/>
</div>
) )
}) })
], ],
[handleDelete] [handleDelete, handleMoveUp, handleMoveDown]
); );
return ( return (

View File

@ -187,7 +187,7 @@ function PickSubstitutions({
}), }),
columnHelper.display({ columnHelper.display({
id: 'status', id: 'status',
size: 40, size: 0,
cell: () => <IconPageRight size='1.2rem' /> cell: () => <IconPageRight size='1.2rem' />
}), }),
columnHelper.accessor(item => item.original.alias, { columnHelper.accessor(item => item.original.alias, {
@ -204,6 +204,7 @@ function PickSubstitutions({
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
size: 0,
cell: props => cell: props =>
props.row.original.is_suggestion ? ( props.row.original.is_suggestion ? (
<div className='max-w-fit'> <div className='max-w-fit'>

View File

@ -260,12 +260,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.localUpdateTimestamp(newData.id); library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
});
} }
}); });
}, },
[itemID, library.localUpdateTimestamp, oss.setData] [itemID, library.reloadItems, oss.setData]
); );
const createInput = useCallback( const createInput = useCallback(

View File

@ -117,6 +117,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
}), }),
argumentsHelper.display({ argumentsHelper.display({
id: 'actions', id: 'actions',
size: 0,
cell: props => ( cell: props => (
<div className='h-[1.25rem] w-[1.25rem]'> <div className='h-[1.25rem] w-[1.25rem]'>
{props.row.original.value ? ( {props.row.original.value ? (
@ -190,7 +191,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
title='Подставить значение аргумента' title='Подставить значение аргумента'
noHover noHover
className='py-0' className='py-0'
icon={<IconAccept size='2rem' className='icon-green' />} icon={<IconAccept size='1.5rem' className='icon-green' />}
disabled={!argumentValue || !selectedArgument} disabled={!argumentValue || !selectedArgument}
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)} onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
/> />
@ -200,7 +201,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
className='py-0' className='py-0'
disabled={!isModified} disabled={!isModified}
onClick={handleReset} onClick={handleReset}
icon={<IconReset size='2rem' className='icon-primary' />} icon={<IconReset size='1.5rem' className='icon-primary' />}
/> />
</div> </div>
</div> </div>

View File

@ -29,9 +29,7 @@ function TableUsers({ items, onDelete }: TableUsersProps) {
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
size: 50, size: 0,
minSize: 50,
maxSize: 50,
cell: props => ( cell: props => (
<div className='h-[1.25rem] w-[1.25rem]'> <div className='h-[1.25rem] w-[1.25rem]'>
<MiniButton <MiniButton

View File

@ -56,9 +56,7 @@ function TableVersions({ processing, items, onDelete, selected, onSelect }: Tabl
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
size: 50, size: 0,
minSize: 50,
maxSize: 50,
cell: props => ( cell: props => (
<div className='h-[1.25rem] w-[1.25rem]'> <div className='h-[1.25rem] w-[1.25rem]'>
<MiniButton <MiniButton

View File

@ -41,18 +41,16 @@ function TableWordForms({ forms, setForms, onFormSelect }: TableWordFormsProps)
size: 350, size: 350,
minSize: 500, minSize: 500,
maxSize: 500, maxSize: 500,
cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div> cell: props => <div className='min-w-[25rem]'>{props.getValue()}</div>
}), }),
columnHelper.accessor('grams', { columnHelper.accessor('grams', {
id: 'grams', id: 'grams',
maxSize: 150, size: 0,
cell: props => <BadgeWordForm keyPrefix={props.cell.id} form={props.row.original} /> cell: props => <BadgeWordForm keyPrefix={props.cell.id} form={props.row.original} />
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
size: 50, size: 0,
minSize: 50,
maxSize: 50,
cell: props => ( cell: props => (
<div className='h-[1.25rem] w-[1.25rem]'> <div className='h-[1.25rem] w-[1.25rem]'>
<MiniButton <MiniButton

View File

@ -4,9 +4,9 @@ import { useIntl } from 'react-intl';
import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons'; import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons';
import InfoUsers from '@/components/info/InfoUsers'; import InfoUsers from '@/components/info/InfoUsers';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
import ValueIcon from '@/components/ui/ValueIcon';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import ValueIcon from '@/components/ui/ValueIcon';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useUsers } from '@/context/UsersContext'; import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';