mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement frontend text refs
This commit is contained in:
parent
38ffba0af6
commit
890e676a2c
|
@ -3,6 +3,8 @@ from typing import Optional, cast
|
|||
from rest_framework import serializers
|
||||
from django.db import transaction
|
||||
|
||||
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
|
||||
|
||||
from .utils import fix_old_references
|
||||
from .models import Constituenta, RSForm
|
||||
|
||||
|
@ -23,6 +25,11 @@ class ExpressionSerializer(serializers.Serializer):
|
|||
expression = serializers.CharField()
|
||||
|
||||
|
||||
class TextSerializer(serializers.Serializer):
|
||||
''' Serializer: Text with references. '''
|
||||
text = serializers.CharField()
|
||||
|
||||
|
||||
class RSFormMetaSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: General purpose RSForm data. '''
|
||||
class Meta:
|
||||
|
@ -288,7 +295,7 @@ class CstRenameSerializer(serializers.ModelSerializer):
|
|||
return attrs
|
||||
|
||||
|
||||
class CstListSerlializer(serializers.Serializer):
|
||||
class CstListSerializer(serializers.Serializer):
|
||||
''' Serializer: List of constituents from one origin. '''
|
||||
items = serializers.ListField(
|
||||
child=CstStandaloneSerializer()
|
||||
|
@ -307,6 +314,41 @@ class CstListSerlializer(serializers.Serializer):
|
|||
return attrs
|
||||
|
||||
|
||||
class CstMoveSerlializer(CstListSerlializer):
|
||||
class CstMoveSerializer(CstListSerializer):
|
||||
''' Serializer: Change constituenta position. '''
|
||||
move_to = serializers.IntegerField()
|
||||
|
||||
|
||||
class ResolverSerializer(serializers.Serializer):
|
||||
''' Serializer: Resolver results serializer. '''
|
||||
def to_representation(self, instance: Resolver) -> dict:
|
||||
return {
|
||||
'input': instance.input,
|
||||
'output': instance.output,
|
||||
'refs': [{
|
||||
'type': str(ref.ref.get_type()),
|
||||
'data': self._get_reference_data(ref.ref),
|
||||
'resolved': ref.resolved,
|
||||
'pos_input': {
|
||||
'start': ref.pos_input.start,
|
||||
'finish': ref.pos_input.finish
|
||||
},
|
||||
'pos_output': {
|
||||
'start': ref.pos_output.start,
|
||||
'finish': ref.pos_output.finish
|
||||
}
|
||||
} for ref in instance.refs]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_reference_data(ref: Reference) -> dict:
|
||||
if ref.get_type() == ReferenceType.entity:
|
||||
return {
|
||||
'entity': cast(EntityReference, ref).entity,
|
||||
'form': cast(EntityReference, ref).form
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'offset': cast(SyntacticReference, ref).offset,
|
||||
'nominal': cast(SyntacticReference, ref).nominal
|
||||
}
|
|
@ -6,6 +6,8 @@ from zipfile import ZipFile
|
|||
from rest_framework.test import APITestCase, APIRequestFactory, APIClient
|
||||
from rest_framework.exceptions import ErrorDetail
|
||||
|
||||
from cctext import ReferenceType
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType
|
||||
from apps.rsform.views import (
|
||||
|
@ -20,6 +22,7 @@ def _response_contains(response, schema: RSForm) -> bool:
|
|||
|
||||
|
||||
class TestConstituentaAPI(APITestCase):
|
||||
''' Testing constituenta view. '''
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create(username='UserTest')
|
||||
|
@ -100,6 +103,7 @@ class TestConstituentaAPI(APITestCase):
|
|||
|
||||
|
||||
class TestRSFormViewset(APITestCase):
|
||||
''' Testing RSForm view. '''
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create(username='UserTest')
|
||||
|
@ -189,6 +193,34 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(response.data['typification'], 'LOGIC')
|
||||
self.assertEqual(response.data['valueClass'], 'value')
|
||||
|
||||
def test_resolve(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
x1 = schema.insert_at(1, 'X1', CstType.BASE)
|
||||
x1.term_resolved = 'синий слон'
|
||||
x1.save()
|
||||
data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'})
|
||||
response = self.client.post(f'/api/rsforms/{schema.id}/resolve/', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
|
||||
self.assertEqual(response.data['output'], 'редким синим слонам')
|
||||
self.assertEqual(len(response.data['refs']), 2)
|
||||
self.assertEqual(response.data['refs'][0]['type'], str(ReferenceType.syntactic))
|
||||
self.assertEqual(response.data['refs'][0]['resolved'], 'редким')
|
||||
self.assertEqual(response.data['refs'][0]['data']['offset'], 1)
|
||||
self.assertEqual(response.data['refs'][0]['data']['nominal'], 'редкий')
|
||||
self.assertEqual(response.data['refs'][0]['pos_input']['start'], 0)
|
||||
self.assertEqual(response.data['refs'][0]['pos_input']['finish'], 11)
|
||||
self.assertEqual(response.data['refs'][0]['pos_output']['start'], 0)
|
||||
self.assertEqual(response.data['refs'][0]['pos_output']['finish'], 6)
|
||||
self.assertEqual(response.data['refs'][1]['type'], str(ReferenceType.entity))
|
||||
self.assertEqual(response.data['refs'][1]['resolved'], 'синим слонам')
|
||||
self.assertEqual(response.data['refs'][1]['data']['entity'], 'X1')
|
||||
self.assertEqual(response.data['refs'][1]['data']['form'], 'plur,datv')
|
||||
self.assertEqual(response.data['refs'][1]['pos_input']['start'], 12)
|
||||
self.assertEqual(response.data['refs'][1]['pos_input']['finish'], 27)
|
||||
self.assertEqual(response.data['refs'][1]['pos_output']['start'], 7)
|
||||
self.assertEqual(response.data['refs'][1]['pos_output']['finish'], 19)
|
||||
|
||||
def test_import_trs(self):
|
||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
|
|
|
@ -111,7 +111,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
def cst_multidelete(self, request, pk):
|
||||
''' Endpoint: Delete multiple constituents. '''
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
||||
serializer = serializers.CstListSerializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.delete_cst(serializer.validated_data['constituents'])
|
||||
schema.refresh_from_db()
|
||||
|
@ -121,7 +121,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
def cst_moveto(self, request, pk):
|
||||
''' Endpoint: Move multiple constituents. '''
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema})
|
||||
serializer = serializers.CstMoveSerializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||
schema.refresh_from_db()
|
||||
|
@ -201,6 +201,17 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
result = pyconcept.check_expression(json.dumps(schema.data), expression)
|
||||
return Response(json.loads(result))
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def resolve(self, request, pk):
|
||||
''' Endpoint: Resolve refenrces in text against schema terms context. '''
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.TextSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
text = serializer.validated_data['text']
|
||||
resolver = schema.resolver()
|
||||
resolver.resolve(text)
|
||||
return Response(status=200, data=serializers.ResolverSerializer(resolver).data)
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||
def export_trs(self, request, pk):
|
||||
''' Endpoint: Download Exteor compatible file. '''
|
||||
|
|
|
@ -5,7 +5,7 @@ from .rumodel import Morphology, SemanticRole, WordTag, morpho, split_grams, com
|
|||
from .ruparser import PhraseParser, WordToken, Collation
|
||||
from .reference import EntityReference, ReferenceType, SyntacticReference, parse_reference
|
||||
from .context import TermForm, Entity, TermContext
|
||||
from .resolver import Position, Resolver, ResolvedReference, resolve_entity, resolve_syntactic, extract_entities
|
||||
from .resolver import Reference, Position, Resolver, ResolvedReference, resolve_entity, resolve_syntactic, extract_entities
|
||||
|
||||
from .conceptapi import (
|
||||
parse, normalize,
|
||||
|
|
|
@ -9,7 +9,7 @@ function MiniButton({ icon, tooltip, children, noHover, ...props }: MiniButtonPr
|
|||
return (
|
||||
<button type='button'
|
||||
title={tooltip}
|
||||
className={`px-1 py-1 font-bold rounded-full cursor-pointer whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear ${noHover ? '' : 'clr-hover'}`}
|
||||
className={`px-1 py-1 font-bold rounded-full cursor-pointer whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear ${noHover ? 'outline-none' : 'clr-hover'}`}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span>{icon}</span>}
|
||||
|
|
76
rsconcept/frontend/src/components/Common/ReferenceInput.tsx
Normal file
76
rsconcept/frontend/src/components/Common/ReferenceInput.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import useResolveText from '../../hooks/useResolveText';
|
||||
import Modal from './Modal';
|
||||
import PrettyJson from './PrettyJSON';
|
||||
import TextArea, { TextAreaProps } from './TextArea';
|
||||
|
||||
interface ReferenceInputProps
|
||||
extends TextAreaProps {
|
||||
initialValue?: string
|
||||
value?: string
|
||||
resolved?: string
|
||||
}
|
||||
|
||||
function ReferenceInput({
|
||||
initialValue, resolved, value,
|
||||
onKeyDown, onChange, onFocus, onBlur, ... props
|
||||
}: ReferenceInputProps) {
|
||||
const { schema } = useRSForm();
|
||||
const { resolveText, refsData } = useResolveText({schema: schema});
|
||||
|
||||
const [showResolve, setShowResolve] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
function handleKeyboard(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (event.altKey) {
|
||||
if (event.key === 'r' && value) {
|
||||
event.preventDefault();
|
||||
resolveText(value, () => {
|
||||
setShowResolve(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (onKeyDown) onKeyDown(event);
|
||||
}
|
||||
|
||||
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
if (onChange) onChange(event);
|
||||
}
|
||||
|
||||
function handleFocusIn(event: React.FocusEvent<HTMLTextAreaElement>) {
|
||||
setIsFocused(true);
|
||||
if (onFocus) onFocus(event);
|
||||
}
|
||||
|
||||
function handleFocusOut(event: React.FocusEvent<HTMLTextAreaElement>) {
|
||||
setIsFocused(false);
|
||||
if (onBlur) onBlur(event);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ showResolve &&
|
||||
<Modal
|
||||
readonly
|
||||
hideWindow={() => setShowResolve(false)}
|
||||
>
|
||||
<div className='max-h-[60vh] max-w-[80vw] overflow-auto'>
|
||||
<PrettyJson data={refsData} />
|
||||
</div>
|
||||
</Modal>}
|
||||
<TextArea
|
||||
value={isFocused ? value : (value !== initialValue ? value : resolved)}
|
||||
onKeyDown={handleKeyboard}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocusIn}
|
||||
onBlur={handleFocusOut}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReferenceInput;
|
|
@ -2,7 +2,7 @@ import { TextareaHTMLAttributes } from 'react';
|
|||
|
||||
import Label from './Label';
|
||||
|
||||
interface TextAreaProps
|
||||
export interface TextAreaProps
|
||||
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
|
||||
label: string
|
||||
widthClass?: string
|
||||
|
|
|
@ -61,7 +61,7 @@ function RSInput({
|
|||
const thisRef = useMemo(
|
||||
() => {
|
||||
return innerref ?? internalRef;
|
||||
}, [internalRef, innerref])
|
||||
}, [internalRef, innerref]);
|
||||
|
||||
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
|
||||
const lightTheme: Extension = useMemo(
|
||||
|
|
31
rsconcept/frontend/src/hooks/useResolveText.ts
Normal file
31
rsconcept/frontend/src/hooks/useResolveText.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
import { DataCallback, postResolveText } from '../utils/backendAPI';
|
||||
import { IReferenceData,IRSForm } from '../utils/models';
|
||||
|
||||
function useResolveText({ schema }: { schema?: IRSForm }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||
const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined);
|
||||
|
||||
const resetData = useCallback(() => { setRefsData(undefined); }, []);
|
||||
|
||||
function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) {
|
||||
setError(undefined);
|
||||
postResolveText(String(schema!.id), {
|
||||
data: { text: text },
|
||||
showError: true,
|
||||
setLoading,
|
||||
onError: error => { setError(error); },
|
||||
onSuccess: data => {
|
||||
setRefsData(data);
|
||||
if (onSuccess) onSuccess(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { refsData, resolveText, resetData, error, setError, loading };
|
||||
}
|
||||
|
||||
export default useResolveText;
|
|
@ -11,7 +11,7 @@ function HomePage() {
|
|||
useLayoutEffect(() => {
|
||||
if (!user) {
|
||||
setTimeout(() => {
|
||||
navigate('/library?filter=common');
|
||||
navigate('/manuals');
|
||||
}, TIMEOUT_UI_REFRESH);
|
||||
} else if(!user.is_staff) {
|
||||
setTimeout(() => {
|
||||
|
@ -22,7 +22,7 @@ function HomePage() {
|
|||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-full py-2'>
|
||||
<p>Home page</p>
|
||||
<p>Лендинг находится в разработке. Данная страница видна только пользователям с правами администратора.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||
import MiniButton from '../../components/Common/MiniButton';
|
||||
import ReferenceInput from '../../components/Common/ReferenceInput';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import TextArea from '../../components/Common/TextArea';
|
||||
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
||||
|
@ -171,10 +172,12 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
|||
icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
|
||||
/>
|
||||
</div>
|
||||
<TextArea id='term' label='Термин'
|
||||
<ReferenceInput id='term' label='Термин'
|
||||
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
|
||||
rows={2}
|
||||
value={term}
|
||||
initialValue={activeCst?.term.raw ?? ''}
|
||||
resolved={activeCst?.term.resolved ?? ''}
|
||||
disabled={!isEnabled}
|
||||
spellCheck
|
||||
onChange={event => setTerm(event.target.value)}
|
||||
|
@ -197,10 +200,12 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
|
|||
setValue={setExpression}
|
||||
setTypification={setTypification}
|
||||
/>
|
||||
<TextArea id='definition' label='Текстовое определение'
|
||||
<ReferenceInput id='definition' label='Текстовое определение'
|
||||
placeholder='Лингвистическая интерпретация формального выражения'
|
||||
rows={4}
|
||||
value={textDefinition}
|
||||
initialValue={activeCst?.definition.text.raw ?? ''}
|
||||
resolved={activeCst?.definition.text.resolved ?? ''}
|
||||
disabled={!isEnabled}
|
||||
spellCheck
|
||||
onChange={event => { setTextDefinition(event.target.value); }}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { config } from './constants'
|
|||
import {
|
||||
IConstituentaList, IConstituentaMeta,
|
||||
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData,
|
||||
ICurrentUser, IExpressionParse, IRSExpression,
|
||||
ICurrentUser, IExpressionParse, IReferenceData, IRefsText, IRSExpression,
|
||||
IRSFormCreateData, IRSFormData,
|
||||
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
|
||||
IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword
|
||||
|
@ -234,6 +234,14 @@ export function postCheckExpression(schema: string, request: FrontExchange<IRSEx
|
|||
});
|
||||
}
|
||||
|
||||
export function postResolveText(schema: string, request: FrontExchange<IRefsText, IReferenceData>) {
|
||||
AxiosPost({
|
||||
title: `Resolve text references for RSForm id=${schema}: ${request.data.text }`,
|
||||
endpoint: `/api/rsforms/${schema}/resolve/`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Reset alias for RSForm id=${target}`,
|
||||
|
|
|
@ -31,6 +31,10 @@ export interface IUserUpdatePassword {
|
|||
}
|
||||
|
||||
// ======== RS Parsing ============
|
||||
export interface IRSExpression {
|
||||
expression: string
|
||||
}
|
||||
|
||||
export enum Syntax {
|
||||
UNDEF = 'undefined',
|
||||
ASCII = 'ascii',
|
||||
|
@ -85,8 +89,42 @@ export interface IExpressionParse {
|
|||
args: IFunctionArg[]
|
||||
}
|
||||
|
||||
export interface IRSExpression {
|
||||
expression: string
|
||||
// ====== Reference resolution =====
|
||||
export interface IRefsText {
|
||||
text: string
|
||||
}
|
||||
|
||||
export enum ReferenceType {
|
||||
ENTITY = 'entity',
|
||||
SYNTACTIC = 'syntax'
|
||||
}
|
||||
|
||||
export interface IEntityReference {
|
||||
entity: string
|
||||
form: string
|
||||
}
|
||||
|
||||
export interface ISyntacticReference {
|
||||
offset: number
|
||||
nominal: string
|
||||
}
|
||||
|
||||
export interface ITextPosition {
|
||||
start: number
|
||||
finish: number
|
||||
}
|
||||
|
||||
export interface IResolvedReference {
|
||||
type: ReferenceType
|
||||
data: IEntityReference | ISyntacticReference
|
||||
pos_input: ITextPosition
|
||||
pos_output: ITextPosition
|
||||
}
|
||||
|
||||
export interface IReferenceData {
|
||||
input: string
|
||||
output: string
|
||||
refs: IResolvedReference[]
|
||||
}
|
||||
|
||||
// ====== Constituenta ==========
|
||||
|
|
Loading…
Reference in New Issue
Block a user