F: Add ordering to OSS arguments
This commit is contained in:
parent
d4fe453840
commit
8f29c69b49
|
@ -14,7 +14,7 @@ class OperationAdmin(admin.ModelAdmin):
|
|||
class ArgumentAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation arguments. '''
|
||||
ordering = ['operation']
|
||||
list_display = ['id', 'operation', 'argument']
|
||||
list_display = ['id', 'order', 'operation', 'argument']
|
||||
search_fields = ['id', 'operation', 'argument']
|
||||
|
||||
|
||||
|
|
18
rsconcept/backend/apps/oss/migrations/0007_argument_order.py
Normal file
18
rsconcept/backend/apps/oss/migrations/0007_argument_order.py
Normal 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='Позиция'),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,5 @@
|
|||
''' 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):
|
||||
|
@ -16,6 +16,10 @@ class Argument(Model):
|
|||
on_delete=CASCADE,
|
||||
related_name='descendants'
|
||||
)
|
||||
order: PositiveIntegerField = PositiveIntegerField(
|
||||
verbose_name='Позиция',
|
||||
default=0,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
|
|
|
@ -137,23 +137,27 @@ class OperationSchema:
|
|||
self.cache.ensure_loaded()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
processed: list[Operation] = []
|
||||
updated: list[Argument] = []
|
||||
deleted: list[Argument] = []
|
||||
for current in operation.getArguments():
|
||||
if current.argument not in arguments:
|
||||
deleted.append(current)
|
||||
else:
|
||||
processed.append(current.argument)
|
||||
current.order = arguments.index(current.argument)
|
||||
updated.append(current)
|
||||
if len(deleted) > 0:
|
||||
self.before_delete_arguments(operation, [x.argument for x in deleted])
|
||||
for deleted_arg in deleted:
|
||||
self.cache.remove_argument(deleted_arg)
|
||||
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
|
||||
Argument.objects.bulk_update(updated, ['order'])
|
||||
|
||||
added: list[Operation] = []
|
||||
for arg in arguments:
|
||||
for order, arg in enumerate(arguments):
|
||||
if arg not in processed:
|
||||
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)
|
||||
added.append(arg)
|
||||
if len(added) > 0:
|
||||
|
@ -219,7 +223,7 @@ class OperationSchema:
|
|||
|
||||
def execute_operation(self, operation: Operation) -> bool:
|
||||
''' 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:
|
||||
return False
|
||||
substitutions = operation.getSubstitutions()
|
||||
|
@ -693,7 +697,7 @@ class OssCache:
|
|||
self.graph = Graph[int]()
|
||||
for operation in self.operations:
|
||||
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.is_loaded = False
|
||||
|
|
|
@ -210,7 +210,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
|||
for operation in oss.operations().order_by('pk'):
|
||||
result['items'].append(OperationSerializer(operation).data)
|
||||
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['substitutions'] = []
|
||||
for substitution in oss.substitutions().values(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' Testing models: Argument. '''
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import Argument, Operation, OperationSchema, OperationType
|
||||
|
@ -36,6 +37,15 @@ class TestArgument(TestCase):
|
|||
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):
|
||||
self.assertEqual(Argument.objects.count(), 1)
|
||||
self.operation2.delete()
|
||||
|
|
|
@ -387,7 +387,7 @@ class TestOssViewset(EndpointTester):
|
|||
'comment': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||
'arguments': [self.operation2.pk, self.operation1.pk],
|
||||
'substitutions': [
|
||||
{
|
||||
'original': self.ks1X1.pk,
|
||||
|
@ -409,7 +409,11 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.operation3.title, data['item_data']['title'])
|
||||
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]
|
||||
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
|
||||
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
||||
|
|
|
@ -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='Позиция'),
|
||||
),
|
||||
]
|
|
@ -58,7 +58,7 @@ class Constituenta(Model):
|
|||
order: PositiveIntegerField = PositiveIntegerField(
|
||||
verbose_name='Позиция',
|
||||
validators=[MinValueValidator(1)],
|
||||
default=-1,
|
||||
default=1,
|
||||
)
|
||||
alias: CharField = CharField(
|
||||
verbose_name='Имя',
|
||||
|
|
|
@ -20,11 +20,6 @@ class TestConstituenta(TestCase):
|
|||
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):
|
||||
with self.assertRaises(IntegrityError):
|
||||
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 |
|
@ -9,7 +9,7 @@ interface BadgeWordFormProps {
|
|||
|
||||
function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) {
|
||||
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 => (
|
||||
<BadgeGrammeme key={`${keyPrefix}-${gram}`} grammeme={gram} />
|
||||
))}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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 DataTable, { createColumnHelper } from '@/components/ui/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -20,7 +20,10 @@ interface PickMultiOperationProps {
|
|||
const columnHelper = createColumnHelper<IOperation>();
|
||||
|
||||
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 [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
|
||||
|
||||
|
@ -40,6 +43,36 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
|
|||
[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(
|
||||
() => [
|
||||
columnHelper.accessor('alias', {
|
||||
|
@ -59,17 +92,35 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 0,
|
||||
cell: props => (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Удалить'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDelete(props.row.original.id)}
|
||||
/>
|
||||
<div className='flex gap-1 w-fit'>
|
||||
<MiniButton
|
||||
noHover
|
||||
className='px-0'
|
||||
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 (
|
||||
|
|
|
@ -187,7 +187,7 @@ function PickSubstitutions({
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
size: 40,
|
||||
size: 0,
|
||||
cell: () => <IconPageRight size='1.2rem' />
|
||||
}),
|
||||
columnHelper.accessor(item => item.original.alias, {
|
||||
|
@ -204,6 +204,7 @@ function PickSubstitutions({
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 0,
|
||||
cell: props =>
|
||||
props.row.original.is_suggestion ? (
|
||||
<div className='max-w-fit'>
|
||||
|
|
|
@ -260,12 +260,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
oss.setData(newData);
|
||||
library.localUpdateTimestamp(newData.id);
|
||||
if (callback) callback();
|
||||
library.reloadItems(() => {
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[itemID, library.localUpdateTimestamp, oss.setData]
|
||||
[itemID, library.reloadItems, oss.setData]
|
||||
);
|
||||
|
||||
const createInput = useCallback(
|
||||
|
|
|
@ -117,6 +117,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
|
|||
}),
|
||||
argumentsHelper.display({
|
||||
id: 'actions',
|
||||
size: 0,
|
||||
cell: props => (
|
||||
<div className='h-[1.25rem] w-[1.25rem]'>
|
||||
{props.row.original.value ? (
|
||||
|
@ -190,7 +191,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
|
|||
title='Подставить значение аргумента'
|
||||
noHover
|
||||
className='py-0'
|
||||
icon={<IconAccept size='2rem' className='icon-green' />}
|
||||
icon={<IconAccept size='1.5rem' className='icon-green' />}
|
||||
disabled={!argumentValue || !selectedArgument}
|
||||
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
|
||||
/>
|
||||
|
@ -200,7 +201,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
|
|||
className='py-0'
|
||||
disabled={!isModified}
|
||||
onClick={handleReset}
|
||||
icon={<IconReset size='2rem' className='icon-primary' />}
|
||||
icon={<IconReset size='1.5rem' className='icon-primary' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,9 +29,7 @@ function TableUsers({ items, onDelete }: TableUsersProps) {
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
maxSize: 50,
|
||||
size: 0,
|
||||
cell: props => (
|
||||
<div className='h-[1.25rem] w-[1.25rem]'>
|
||||
<MiniButton
|
||||
|
|
|
@ -56,9 +56,7 @@ function TableVersions({ processing, items, onDelete, selected, onSelect }: Tabl
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
maxSize: 50,
|
||||
size: 0,
|
||||
cell: props => (
|
||||
<div className='h-[1.25rem] w-[1.25rem]'>
|
||||
<MiniButton
|
||||
|
|
|
@ -41,18 +41,16 @@ function TableWordForms({ forms, setForms, onFormSelect }: TableWordFormsProps)
|
|||
size: 350,
|
||||
minSize: 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', {
|
||||
id: 'grams',
|
||||
maxSize: 150,
|
||||
size: 0,
|
||||
cell: props => <BadgeWordForm keyPrefix={props.cell.id} form={props.row.original} />
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
maxSize: 50,
|
||||
size: 0,
|
||||
cell: props => (
|
||||
<div className='h-[1.25rem] w-[1.25rem]'>
|
||||
<MiniButton
|
||||
|
|
|
@ -4,9 +4,9 @@ import { useIntl } from 'react-intl';
|
|||
import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons';
|
||||
import InfoUsers from '@/components/info/InfoUsers';
|
||||
import SelectUser from '@/components/select/SelectUser';
|
||||
import ValueIcon from '@/components/ui/ValueIcon';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
import ValueIcon from '@/components/ui/ValueIcon';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
|
|
Loading…
Reference in New Issue
Block a user