Compare commits
54 Commits
266fdf0c30
...
6b2268a76b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b2268a76b | ||
|
|
22eb2a482c | ||
|
|
b55f33c17d | ||
|
|
890b1894e8 | ||
|
|
0743cffdd7 | ||
|
|
de59f7e336 | ||
|
|
3b3cb06a40 | ||
|
|
ff04d006ea | ||
|
|
bed44b57ca | ||
|
|
925147299e | ||
|
|
c3e17549c7 | ||
|
|
26939c025b | ||
|
|
eb28640513 | ||
|
|
2659568e3d | ||
|
|
9811d0155e | ||
|
|
9f1aa2b4a8 | ||
|
|
c634ae9700 | ||
|
|
fb1bff055c | ||
|
|
f9d0f8556c | ||
|
|
d1fae22f90 | ||
|
|
99d368fc54 | ||
|
|
6e17eada27 | ||
|
|
c358415642 | ||
|
|
1555a1bf92 | ||
|
|
14cda60b0d | ||
|
|
1d182f4417 | ||
|
|
5836e48b22 | ||
|
|
21c55c6a43 | ||
|
|
eebea31a74 | ||
|
|
eadcea566b | ||
|
|
d27d7bc31e | ||
|
|
7d12c12815 | ||
|
|
be54dde982 | ||
|
|
866412eb98 | ||
|
|
a6f36d0d4f | ||
|
|
f8dd26cc4c | ||
|
|
17f9658e97 | ||
|
|
c6f52ed8f4 | ||
|
|
c7ac37411d | ||
|
|
e20f969bfd | ||
|
|
926b133507 | ||
|
|
78861cd224 | ||
|
|
a48f1f406d | ||
|
|
bf3df7903e | ||
|
|
7a2b8a0212 | ||
|
|
ad0f30b5fd | ||
|
|
1c2d4339fb | ||
|
|
b0481c2cbd | ||
|
|
4588cd5f69 | ||
|
|
95ec09a5e0 | ||
|
|
25777b8efc | ||
|
|
95c9d58f43 | ||
|
|
78ccc613bd | ||
|
|
f8a573bbc8 |
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
|
|
@ -177,11 +177,16 @@
|
||||||
"Upvote",
|
"Upvote",
|
||||||
"Viewset",
|
"Viewset",
|
||||||
"viewsets",
|
"viewsets",
|
||||||
|
"vkvideo",
|
||||||
"wordform",
|
"wordform",
|
||||||
"Wordforms",
|
"Wordforms",
|
||||||
|
"XCSDATN",
|
||||||
"Айзенштат",
|
"Айзенштат",
|
||||||
"Акименков",
|
"Акименков",
|
||||||
"Астрина",
|
"Астрина",
|
||||||
|
"Атрибутирование",
|
||||||
|
"Атрибутирующая",
|
||||||
|
"Атрибутирующие",
|
||||||
"Ашихмин",
|
"Ашихмин",
|
||||||
"Биективная",
|
"Биективная",
|
||||||
"биективной",
|
"биективной",
|
||||||
|
|
@ -191,6 +196,8 @@
|
||||||
"Бурбакизатор",
|
"Бурбакизатор",
|
||||||
"Версионирование",
|
"Версионирование",
|
||||||
"Владельцом",
|
"Владельцом",
|
||||||
|
"генемные",
|
||||||
|
"дебуль",
|
||||||
"Демешко",
|
"Демешко",
|
||||||
"Десинглетон",
|
"Десинглетон",
|
||||||
"доксинг",
|
"доксинг",
|
||||||
|
|
@ -212,9 +219,16 @@
|
||||||
"Кучкаров",
|
"Кучкаров",
|
||||||
"Кучкарова",
|
"Кучкарова",
|
||||||
"мультиграфа",
|
"мультиграфа",
|
||||||
|
"мультииндекс",
|
||||||
|
"Мультииндексы",
|
||||||
|
"Мультифильтр",
|
||||||
"неинтерпретируемый",
|
"неинтерпретируемый",
|
||||||
"неитерируемого",
|
"неитерируемого",
|
||||||
"Никанорова",
|
"Никанорова",
|
||||||
|
"Номиноид",
|
||||||
|
"номиноида",
|
||||||
|
"номиноидом",
|
||||||
|
"Номиноиды",
|
||||||
"операционализации",
|
"операционализации",
|
||||||
"операционализированных",
|
"операционализированных",
|
||||||
"Оргтеор",
|
"Оргтеор",
|
||||||
|
|
@ -224,6 +238,7 @@
|
||||||
"подпапках",
|
"подпапках",
|
||||||
"Присакарь",
|
"Присакарь",
|
||||||
"ПРОКСИМА",
|
"ПРОКСИМА",
|
||||||
|
"родовидовое",
|
||||||
"Родоструктурная",
|
"Родоструктурная",
|
||||||
"родоструктурного",
|
"родоструктурного",
|
||||||
"Родоструктурное",
|
"Родоструктурное",
|
||||||
|
|
|
||||||
18
TODO.txt
18
TODO.txt
|
|
@ -1,33 +1,29 @@
|
||||||
!! This is not complete list of TODOs !!
|
|
||||||
For more specific TODOs see comments in code
|
|
||||||
|
|
||||||
[Bugs - PENDING]
|
[Bugs - PENDING]
|
||||||
-
|
-
|
||||||
|
|
||||||
[Functionality - PENDING]
|
[Functionality - PENDING]
|
||||||
|
- Export PDF (Items list, Graph)
|
||||||
|
- Save react-flow to vector image
|
||||||
- Landing page
|
- Landing page
|
||||||
- Design first user experience
|
- Design first user experience
|
||||||
|
- Video guides
|
||||||
- Demo sandbox for anonymous users
|
- Demo sandbox for anonymous users
|
||||||
- Save react-flow to vector image
|
|
||||||
|
- Implement rslang and rsmodel functionality in the frontend
|
||||||
|
- Allow manual setup for typification and value class
|
||||||
|
|
||||||
User profile:
|
User profile:
|
||||||
- Settings server persistency
|
- Settings server persistency
|
||||||
- Profile pictures
|
- Profile pictures (avatars)
|
||||||
- Custom LibraryItem lists
|
- Custom LibraryItem lists
|
||||||
- Custom user filters and sharing filters
|
- Custom user filters and sharing filters
|
||||||
- Personal prompt templates
|
|
||||||
|
|
||||||
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
|
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
|
||||||
- OSS clone and versioning
|
|
||||||
- Clone with saving info connection
|
|
||||||
- Semantic diff for library items
|
|
||||||
|
|
||||||
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
|
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
|
||||||
- Draggable rows in constituents table
|
- Draggable rows in constituents table
|
||||||
|
|
||||||
- Search functionality for Help Manuals - use google search integration filtered by site?
|
- Search functionality for Help Manuals - use google search integration filtered by site?
|
||||||
- Export PDF (Items list, Graph)
|
|
||||||
- ARIA (accessibility considerations) - for now machine reading not supported
|
|
||||||
- Internationalization - at least english version. Consider react.intl
|
- Internationalization - at least english version. Consider react.intl
|
||||||
- Sitemap for better SEO and crawler optimization
|
- Sitemap for better SEO and crawler optimization
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from apps.library.models import (
|
||||||
LibraryTemplate,
|
LibraryTemplate,
|
||||||
LocationHead
|
LocationHead
|
||||||
)
|
)
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import Attribution, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
from shared.testing_utils import response_contains
|
from shared.testing_utils import response_contains
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
'title': 'Title',
|
'title': 'Title',
|
||||||
'alias': 'alias',
|
'alias': 'alias',
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
self.assertEqual(response.data['item_type'], LibraryItemType.RSFORM)
|
self.assertEqual(response.data['item_type'], LibraryItemType.RSFORM)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
|
@ -57,7 +57,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
'visible': False,
|
'visible': False,
|
||||||
'read_only': True
|
'read_only': True
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
oss = LibraryItem.objects.get(pk=response.data['id'])
|
oss = LibraryItem.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(oss.owner, self.user)
|
self.assertEqual(oss.owner, self.user)
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
|
|
@ -70,25 +70,25 @@ class TestLibraryViewset(EndpointTester):
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
data = {'title': 'Title2'}
|
data = {'title': 'Title2'}
|
||||||
self.executeForbidden(data=data)
|
self.executeForbidden(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}', method='patch')
|
@decl_endpoint('/api/library/{item}', method='patch')
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
data = {'title': 'New Title'}
|
data = {'title': 'New Title'}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
self.toggle_editor(self.unowned, True)
|
self.toggle_editor(self.unowned, True)
|
||||||
response = self.executeOK(data=data, item=self.unowned.pk)
|
response = self.executeOK(data, item=self.unowned.pk)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
|
||||||
self.unowned.access_policy = AccessPolicy.PRIVATE
|
self.unowned.access_policy = AccessPolicy.PRIVATE
|
||||||
self.unowned.save()
|
self.unowned.save()
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
data = {'title': 'New Title'}
|
data = {'title': 'New Title'}
|
||||||
response = self.executeOK(data=data, item=self.owned.pk)
|
response = self.executeOK(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
self.assertEqual(response.data['alias'], self.owned.alias)
|
self.assertEqual(response.data['alias'], self.owned.alias)
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
'access_policy': AccessPolicy.PROTECTED,
|
'access_policy': AccessPolicy.PROTECTED,
|
||||||
'location': LocationHead.LIBRARY
|
'location': LocationHead.LIBRARY
|
||||||
}
|
}
|
||||||
response = self.executeOK(data=data, item=self.owned.pk)
|
response = self.executeOK(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
||||||
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
||||||
|
|
@ -111,22 +111,22 @@ class TestLibraryViewset(EndpointTester):
|
||||||
time_update = self.owned.time_update
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
data = {'user': self.user.pk}
|
data = {'user': self.user.pk}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.owner, self.user)
|
self.assertEqual(self.owned.owner, self.user)
|
||||||
|
|
||||||
data = {'user': self.user2.pk}
|
data = {'user': self.user2.pk}
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.owner, self.user2)
|
self.assertEqual(self.owned.owner, self.user2)
|
||||||
self.assertEqual(self.owned.time_update, time_update)
|
self.assertEqual(self.owned.time_update, time_update)
|
||||||
self.executeForbidden(data=data, item=self.owned.pk)
|
self.executeForbidden(data, item=self.owned.pk)
|
||||||
|
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
data = {'user': self.user.pk}
|
data = {'user': self.user.pk}
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.owner, self.user)
|
self.assertEqual(self.owned.owner, self.user)
|
||||||
|
|
||||||
|
|
@ -135,20 +135,20 @@ class TestLibraryViewset(EndpointTester):
|
||||||
time_update = self.owned.time_update
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
data = {'access_policy': 'invalid'}
|
data = {'access_policy': 'invalid'}
|
||||||
self.executeBadData(data=data, item=self.owned.pk)
|
self.executeBadData(data, item=self.owned.pk)
|
||||||
|
|
||||||
data = {'access_policy': AccessPolicy.PRIVATE}
|
data = {'access_policy': AccessPolicy.PRIVATE}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.access_policy, data['access_policy'])
|
self.assertEqual(self.owned.access_policy, data['access_policy'])
|
||||||
|
|
||||||
self.toggle_editor(self.unowned, True)
|
self.toggle_editor(self.unowned, True)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeOK(data=data, item=self.unowned.pk)
|
self.executeOK(data, item=self.unowned.pk)
|
||||||
self.unowned.refresh_from_db()
|
self.unowned.refresh_from_db()
|
||||||
self.assertEqual(self.unowned.access_policy, data['access_policy'])
|
self.assertEqual(self.unowned.access_policy, data['access_policy'])
|
||||||
|
|
||||||
|
|
@ -157,29 +157,29 @@ class TestLibraryViewset(EndpointTester):
|
||||||
time_update = self.owned.time_update
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
data = {'location': 'invalid'}
|
data = {'location': 'invalid'}
|
||||||
self.executeBadData(data=data, item=self.owned.pk)
|
self.executeBadData(data, item=self.owned.pk)
|
||||||
|
|
||||||
data = {'location': '/U/temp'}
|
data = {'location': '/U/temp'}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.location, data['location'])
|
self.assertEqual(self.owned.location, data['location'])
|
||||||
|
|
||||||
data = {'location': LocationHead.LIBRARY}
|
data = {'location': LocationHead.LIBRARY}
|
||||||
self.executeForbidden(data=data, item=self.owned.pk)
|
self.executeForbidden(data, item=self.owned.pk)
|
||||||
|
|
||||||
data = {'location': '/U/temp'}
|
data = {'location': '/U/temp'}
|
||||||
self.toggle_editor(self.unowned, True)
|
self.toggle_editor(self.unowned, True)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
data = {'location': LocationHead.LIBRARY}
|
data = {'location': LocationHead.LIBRARY}
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.location, data['location'])
|
self.assertEqual(self.owned.location, data['location'])
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.unowned.pk)
|
self.executeOK(data, item=self.unowned.pk)
|
||||||
self.unowned.refresh_from_db()
|
self.unowned.refresh_from_db()
|
||||||
self.assertEqual(self.unowned.location, data['location'])
|
self.assertEqual(self.unowned.location, data['location'])
|
||||||
|
|
||||||
|
|
@ -201,12 +201,12 @@ class TestLibraryViewset(EndpointTester):
|
||||||
'new_location': '/S/temp2'
|
'new_location': '/S/temp2'
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeBadData(data={})
|
self.executeBadData({})
|
||||||
self.executeBadData(data={'target:': '/S/temp'})
|
self.executeBadData({'target:': '/S/temp'})
|
||||||
self.executeBadData(data={'new_location:': '/S/temp'})
|
self.executeBadData({'new_location:': '/S/temp'})
|
||||||
self.executeBadData(data={'target:': 'invalid', 'new_location': '/S/temp'})
|
self.executeBadData({'target:': 'invalid', 'new_location': '/S/temp'})
|
||||||
self.executeBadData(data={'target:': '/S/temp', 'new_location': 'invalid'})
|
self.executeBadData({'target:': '/S/temp', 'new_location': 'invalid'})
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.unowned.refresh_from_db()
|
self.unowned.refresh_from_db()
|
||||||
owned2.refresh_from_db()
|
owned2.refresh_from_db()
|
||||||
|
|
@ -215,7 +215,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.assertEqual(owned2.location, '/S/temp2/123')
|
self.assertEqual(owned2.location, '/S/temp2/123')
|
||||||
|
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.unowned.refresh_from_db()
|
self.unowned.refresh_from_db()
|
||||||
self.assertEqual(self.unowned.location, '/S/temp2')
|
self.assertEqual(self.unowned.location, '/S/temp2')
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
}
|
}
|
||||||
|
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.unowned.refresh_from_db()
|
self.unowned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.location, '/U/temp2')
|
self.assertEqual(self.owned.location, '/U/temp2')
|
||||||
|
|
@ -243,30 +243,30 @@ class TestLibraryViewset(EndpointTester):
|
||||||
time_update = self.owned.time_update
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
data = {'users': [self.invalid_user]}
|
data = {'users': [self.invalid_user]}
|
||||||
self.executeBadData(data=data, item=self.owned.pk)
|
self.executeBadData(data, item=self.owned.pk)
|
||||||
|
|
||||||
data = {'users': [self.user.pk]}
|
data = {'users': [self.user.pk]}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned.pk)
|
self.executeOK(data, item=self.owned.pk)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.time_update, time_update)
|
self.assertEqual(self.owned.time_update, time_update)
|
||||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
||||||
|
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
||||||
|
|
||||||
data = {'users': [self.user2.pk]}
|
data = {'users': [self.user2.pk]}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user2])
|
self.assertEqual(list(self.owned.getQ_editors()), [self.user2])
|
||||||
|
|
||||||
data = {'users': []}
|
data = {'users': []}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertEqual(list(self.owned.getQ_editors()), [])
|
self.assertEqual(list(self.owned.getQ_editors()), [])
|
||||||
|
|
||||||
data = {'users': [self.user2.pk, self.user.pk]}
|
data = {'users': [self.user2.pk, self.user.pk]}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertEqual(set(self.owned.getQ_editors()), set([self.user2, self.user]))
|
self.assertEqual(set(self.owned.getQ_editors()), set([self.user2, self.user]))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -343,14 +343,16 @@ class TestLibraryViewset(EndpointTester):
|
||||||
term_raw='@{X12|plur}',
|
term_raw='@{X12|plur}',
|
||||||
term_resolved='люди'
|
term_resolved='люди'
|
||||||
)
|
)
|
||||||
|
Attribution.objects.create(container=d2, attribute=x12)
|
||||||
|
|
||||||
data = {'item_data': {'title': 'Title1337'}, 'items': []}
|
data = {'item_data': {'title': 'Title1337'}, 'items': []}
|
||||||
self.executeNotFound(data=data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeCreated(data=data, item=self.unowned.pk)
|
self.executeCreated(data, item=self.unowned.pk)
|
||||||
|
|
||||||
response = self.executeCreated(data=data, item=self.owned.pk)
|
response = self.executeCreated(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], data['item_data']['title'])
|
self.assertEqual(response.data['title'], data['item_data']['title'])
|
||||||
self.assertEqual(len(response.data['items']), 2)
|
self.assertEqual(len(response.data['items']), 2)
|
||||||
|
self.assertEqual(len(response.data['attribution']), 1)
|
||||||
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
||||||
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
|
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
|
||||||
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
||||||
|
|
@ -358,12 +360,12 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
||||||
|
|
||||||
data = {'item_data': {'title': 'Title1340'}, 'items': []}
|
data = {'item_data': {'title': 'Title1340'}, 'items': []}
|
||||||
response = self.executeCreated(data=data, item=self.owned.pk)
|
response = self.executeCreated(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], data['item_data']['title'])
|
self.assertEqual(response.data['title'], data['item_data']['title'])
|
||||||
self.assertEqual(len(response.data['items']), 2)
|
self.assertEqual(len(response.data['items']), 2)
|
||||||
|
|
||||||
data = {'item_data': {'title': 'Title1341'}, 'items': [x12.pk]}
|
data = {'item_data': {'title': 'Title1341'}, 'items': [x12.pk]}
|
||||||
response = self.executeCreated(data=data, item=self.owned.pk)
|
response = self.executeCreated(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], data['item_data']['title'])
|
self.assertEqual(response.data['title'], data['item_data']['title'])
|
||||||
self.assertEqual(len(response.data['items']), 1)
|
self.assertEqual(len(response.data['items']), 1)
|
||||||
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,11 @@ class TestVersionViews(EndpointTester):
|
||||||
invalid_id = 1338
|
invalid_id = 1338
|
||||||
data = {'version': '1.0.0', 'description': 'test'}
|
data = {'version': '1.0.0', 'description': 'test'}
|
||||||
|
|
||||||
self.executeNotFound(data=data, schema=invalid_id)
|
self.executeNotFound(data, schema=invalid_id)
|
||||||
self.executeForbidden(data=data, schema=self.unowned_id)
|
self.executeForbidden(data, schema=self.unowned_id)
|
||||||
self.executeBadData(data=invalid_data, schema=self.owned_id)
|
self.executeBadData(invalid_data, schema=self.owned_id)
|
||||||
|
|
||||||
response = self.executeCreated(data=data, schema=self.owned_id)
|
response = self.executeCreated(data, schema=self.owned_id)
|
||||||
self.assertTrue('version' in response.data)
|
self.assertTrue('version' in response.data)
|
||||||
self.assertTrue('schema' in response.data)
|
self.assertTrue('schema' in response.data)
|
||||||
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
||||||
|
|
@ -46,7 +46,7 @@ class TestVersionViews(EndpointTester):
|
||||||
def test_create_version_filter(self):
|
def test_create_version_filter(self):
|
||||||
x2 = self.owned.insert_last('X2')
|
x2 = self.owned.insert_last('X2')
|
||||||
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
|
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
|
||||||
response = self.executeCreated(data=data, schema=self.owned_id)
|
response = self.executeCreated(data, schema=self.owned_id)
|
||||||
version = Version.objects.get(pk=response.data['version'])
|
version = Version.objects.get(pk=response.data['version'])
|
||||||
items = version.data['items']
|
items = version.data['items']
|
||||||
self.assertTrue('version' in response.data)
|
self.assertTrue('version' in response.data)
|
||||||
|
|
@ -102,7 +102,7 @@ class TestVersionViews(EndpointTester):
|
||||||
@decl_endpoint('/api/versions/{version}', method='get')
|
@decl_endpoint('/api/versions/{version}', method='get')
|
||||||
def test_access_version(self):
|
def test_access_version(self):
|
||||||
data = {'version': '1.0.0', 'description': 'test'}
|
data = {'version': '1.0.0', 'description': 'test'}
|
||||||
version_id = self._create_version(data=data)
|
version_id = self._create_version(data)
|
||||||
invalid_id = version_id + 1337
|
invalid_id = version_id + 1337
|
||||||
|
|
||||||
self.executeNotFound(version=invalid_id)
|
self.executeNotFound(version=invalid_id)
|
||||||
|
|
@ -116,14 +116,14 @@ class TestVersionViews(EndpointTester):
|
||||||
|
|
||||||
data = {'version': '1.2.0', 'description': 'test1'}
|
data = {'version': '1.2.0', 'description': 'test1'}
|
||||||
self.method = 'patch'
|
self.method = 'patch'
|
||||||
self.executeForbidden(data=data)
|
self.executeForbidden(data)
|
||||||
|
|
||||||
self.method = 'delete'
|
self.method = 'delete'
|
||||||
self.executeForbidden()
|
self.executeForbidden()
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
self.method = 'patch'
|
self.method = 'patch'
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
response = self.get()
|
response = self.get()
|
||||||
self.assertEqual(response.data['version'], data['version'])
|
self.assertEqual(response.data['version'], data['version'])
|
||||||
self.assertEqual(response.data['description'], data['description'])
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
@ -157,7 +157,7 @@ class TestVersionViews(EndpointTester):
|
||||||
x2 = self.owned.insert_last('X2')
|
x2 = self.owned.insert_last('X2')
|
||||||
d1 = self.owned.insert_last('D1', term_raw='TestTerm')
|
d1 = self.owned.insert_last('D1', term_raw='TestTerm')
|
||||||
data = {'version': '1.0.0', 'description': 'test'}
|
data = {'version': '1.0.0', 'description': 'test'}
|
||||||
version_id = self._create_version(data=data)
|
version_id = self._create_version(data)
|
||||||
invalid_id = version_id + 1337
|
invalid_id = version_id + 1337
|
||||||
|
|
||||||
Constituenta.objects.get(pk=d1.pk).delete()
|
Constituenta.objects.get(pk=d1.pk).delete()
|
||||||
|
|
@ -186,7 +186,7 @@ class TestVersionViews(EndpointTester):
|
||||||
def _create_version(self, data) -> int:
|
def _create_version(self, data) -> int:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f'/api/library/{self.owned_id}/create-version',
|
f'/api/library/{self.owned_id}/create-version',
|
||||||
data=data, format='json'
|
data, format='json'
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
return response.data['version'] # type: ignore
|
return response.data['version'] # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
||||||
from apps.rsform.models import RSFormCached
|
from apps.rsform.models import Attribution, RSFormCached
|
||||||
from apps.rsform.serializers import RSFormParseSerializer
|
from apps.rsform.serializers import RSFormParseSerializer
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared import permissions
|
from shared import permissions
|
||||||
|
|
@ -157,8 +157,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
serializer = s.LibraryItemCloneSerializer(data=request.data, context={'schema': item})
|
serializer = s.LibraryItemCloneSerializer(data=request.data, context={'schema': item})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
data = serializer.validated_data['item_data']
|
data = serializer.validated_data['item_data']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
clone = deepcopy(item)
|
clone = deepcopy(item)
|
||||||
clone.pk = None
|
clone.pk = None
|
||||||
|
|
@ -171,12 +171,24 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
|
clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
|
||||||
clone.location = data.get('location', m.LocationHead.USER)
|
clone.location = data.get('location', m.LocationHead.USER)
|
||||||
clone.save()
|
clone.save()
|
||||||
need_filter = 'items' in request.data and len(request.data['items']) > 0
|
|
||||||
|
cst_map: dict[int, int] = {}
|
||||||
|
cst_list: list[int] = []
|
||||||
|
need_filter = 'items' in request.data and request.data['items']
|
||||||
for cst in RSFormCached(item).constituentsQ():
|
for cst in RSFormCached(item).constituentsQ():
|
||||||
if not need_filter or cst.pk in request.data['items']:
|
if not need_filter or cst.pk in request.data['items']:
|
||||||
|
old_pk = cst.pk
|
||||||
cst.pk = None
|
cst.pk = None
|
||||||
cst.schema = clone
|
cst.schema = clone
|
||||||
cst.save()
|
cst.save()
|
||||||
|
cst_map[old_pk] = cst.pk
|
||||||
|
cst_list.append(old_pk)
|
||||||
|
for attr in Attribution.objects.filter(container__in=cst_list, attribute__in=cst_list):
|
||||||
|
attr.pk = None
|
||||||
|
attr.container_id = cst_map[attr.container_id]
|
||||||
|
attr.attribute_id = cst_map[attr.attribute_id]
|
||||||
|
attr.save()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data=RSFormParseSerializer(clone).data
|
data=RSFormParseSerializer(clone).data
|
||||||
|
|
@ -299,7 +311,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
|
added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
|
||||||
if len(added) >= 0 or len(deleted) >= 0:
|
if added or deleted:
|
||||||
owned_schemas = OperationSchema.owned_schemasQ(item).only('pk')
|
owned_schemas = OperationSchema.owned_schemasQ(item).only('pk')
|
||||||
if owned_schemas.exists():
|
if owned_schemas.exists():
|
||||||
m.Editor.objects.filter(
|
m.Editor.objects.filter(
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ class InheritanceAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['id', 'operation', 'parent', 'child']
|
search_fields = ['id', 'operation', 'parent', 'child']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Reference)
|
@admin.register(models.Replica)
|
||||||
class ReferenceAdmin(admin.ModelAdmin):
|
class ReplicaAdmin(admin.ModelAdmin):
|
||||||
''' Admin model: Reference. '''
|
''' Admin model: Replica. '''
|
||||||
ordering = ['reference', 'target']
|
ordering = ['replica', 'original']
|
||||||
list_display = ['id', 'reference', 'target']
|
list_display = ['id', 'replica', 'original']
|
||||||
search_fields = ['id', 'reference', 'target']
|
search_fields = ['id', 'replica', 'original']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-08-06 09:10
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oss', '0015_reference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operation',
|
||||||
|
name='operation_type',
|
||||||
|
field=models.CharField(choices=[('input', 'Input'), ('synthesis', 'Synthesis'), ('replica', 'Replica')], default='input', max_length=10, verbose_name='Тип'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Replica',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targets', to='oss.operation', verbose_name='Целевая Операция')),
|
||||||
|
('replica', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replicas', to='oss.operation', verbose_name='Реплика')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Реплика',
|
||||||
|
'verbose_name_plural': 'Реплики',
|
||||||
|
'unique_together': {('replica', 'original')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Reference',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -16,7 +16,7 @@ from django.db.models import (
|
||||||
from apps.library.models import LibraryItem
|
from apps.library.models import LibraryItem
|
||||||
|
|
||||||
from .Argument import Argument
|
from .Argument import Argument
|
||||||
from .Reference import Reference
|
from .Replica import Replica
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ class OperationType(TextChoices):
|
||||||
''' Type of operation. '''
|
''' Type of operation. '''
|
||||||
INPUT = 'input'
|
INPUT = 'input'
|
||||||
SYNTHESIS = 'synthesis'
|
SYNTHESIS = 'synthesis'
|
||||||
REFERENCE = 'reference'
|
REPLICA = 'replica'
|
||||||
|
|
||||||
|
|
||||||
class Operation(Model):
|
class Operation(Model):
|
||||||
|
|
@ -93,13 +93,13 @@ class Operation(Model):
|
||||||
''' Operation substitutions. '''
|
''' Operation substitutions. '''
|
||||||
return Substitution.objects.filter(operation=self)
|
return Substitution.objects.filter(operation=self)
|
||||||
|
|
||||||
def getQ_references(self) -> QuerySet[Reference]:
|
def getQ_replicas(self) -> QuerySet[Replica]:
|
||||||
''' Operation references. '''
|
''' Operation replicas. '''
|
||||||
return Reference.objects.filter(target=self)
|
return Replica.objects.filter(original=self)
|
||||||
|
|
||||||
def getQ_reference_target(self) -> list['Operation']:
|
def getQ_replica_original(self) -> list['Operation']:
|
||||||
''' Operation target for current reference. '''
|
''' Operation source for current replica. '''
|
||||||
return [x.target for x in Reference.objects.filter(reference=self)]
|
return [x.original for x in Replica.objects.filter(replica=self)]
|
||||||
|
|
||||||
def setQ_result(self, result: Optional[LibraryItem]) -> None:
|
def setQ_result(self, result: Optional[LibraryItem]) -> None:
|
||||||
''' Set result schema. '''
|
''' Set result schema. '''
|
||||||
|
|
@ -107,12 +107,12 @@ class Operation(Model):
|
||||||
return
|
return
|
||||||
self.result = result
|
self.result = result
|
||||||
self.save(update_fields=['result'])
|
self.save(update_fields=['result'])
|
||||||
for reference in self.getQ_references():
|
for rep in self.getQ_replicas():
|
||||||
reference.reference.result = result
|
rep.replica.result = result
|
||||||
reference.reference.save(update_fields=['result'])
|
rep.replica.save(update_fields=['result'])
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
''' Delete operation. '''
|
''' Delete operation. '''
|
||||||
for ref in self.getQ_references():
|
for rep in self.getQ_replicas():
|
||||||
ref.reference.delete()
|
rep.replica.delete()
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from .Block import Block
|
||||||
from .Inheritance import Inheritance
|
from .Inheritance import Inheritance
|
||||||
from .Layout import Layout
|
from .Layout import Layout
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation, OperationType
|
||||||
from .Reference import Reference
|
from .Replica import Replica
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,6 +42,22 @@ class OperationSchema:
|
||||||
''' OSS layout. '''
|
''' OSS layout. '''
|
||||||
return Layout.objects.get(oss_id=itemID)
|
return Layout.objects.get(oss_id=itemID)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_input(oss: LibraryItem, operation: Operation) -> RSFormCached:
|
||||||
|
''' Create input RSForm for given Operation. '''
|
||||||
|
schema = RSFormCached.create(
|
||||||
|
owner=oss.owner,
|
||||||
|
alias=operation.alias,
|
||||||
|
title=operation.title,
|
||||||
|
description=operation.description,
|
||||||
|
visible=False,
|
||||||
|
access_policy=oss.access_policy,
|
||||||
|
location=oss.location
|
||||||
|
)
|
||||||
|
Editor.set(schema.model.pk, oss.getQ_editors().values_list('pk', flat=True))
|
||||||
|
operation.setQ_result(schema.model)
|
||||||
|
return schema
|
||||||
|
|
||||||
def refresh_from_db(self) -> None:
|
def refresh_from_db(self) -> None:
|
||||||
''' Model wrapper. '''
|
''' Model wrapper. '''
|
||||||
self.model.refresh_from_db()
|
self.model.refresh_from_db()
|
||||||
|
|
@ -51,15 +67,15 @@ class OperationSchema:
|
||||||
result = Operation.objects.create(oss=self.model, **kwargs)
|
result = Operation.objects.create(oss=self.model, **kwargs)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def create_reference(self, target: Operation) -> Operation:
|
def create_replica(self, target: Operation) -> Operation:
|
||||||
''' Create Reference Operation. '''
|
''' Create Replica Operation. '''
|
||||||
result = Operation.objects.create(
|
result = Operation.objects.create(
|
||||||
oss=self.model,
|
oss=self.model,
|
||||||
operation_type=OperationType.REFERENCE,
|
operation_type=OperationType.REPLICA,
|
||||||
result=target.result,
|
result=target.result,
|
||||||
parent=target.parent
|
parent=target.parent
|
||||||
)
|
)
|
||||||
Reference.objects.create(reference=result, target=target)
|
Replica.objects.create(replica=result, original=target)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def create_block(self, **kwargs) -> Block:
|
def create_block(self, **kwargs) -> Block:
|
||||||
|
|
@ -80,21 +96,6 @@ class OperationSchema:
|
||||||
operation.save(update_fields=['parent'])
|
operation.save(update_fields=['parent'])
|
||||||
target.delete()
|
target.delete()
|
||||||
|
|
||||||
def create_input(self, operation: Operation) -> RSFormCached:
|
|
||||||
''' Create input RSForm for given Operation. '''
|
|
||||||
schema = RSFormCached.create(
|
|
||||||
owner=self.model.owner,
|
|
||||||
alias=operation.alias,
|
|
||||||
title=operation.title,
|
|
||||||
description=operation.description,
|
|
||||||
visible=False,
|
|
||||||
access_policy=self.model.access_policy,
|
|
||||||
location=self.model.location
|
|
||||||
)
|
|
||||||
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
|
|
||||||
operation.setQ_result(schema.model)
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||||
''' Set arguments of target Operation. '''
|
''' Set arguments of target Operation. '''
|
||||||
Argument.objects.filter(operation_id=target).delete()
|
Argument.objects.filter(operation_id=target).delete()
|
||||||
|
|
@ -128,10 +129,10 @@ class OperationSchema:
|
||||||
.order_by('order')
|
.order_by('order')
|
||||||
if arg.argument.result_id is not None
|
if arg.argument.result_id is not None
|
||||||
]
|
]
|
||||||
if len(schemas) == 0:
|
if not schemas:
|
||||||
return
|
return
|
||||||
substitutions = operation.getQ_substitutions()
|
substitutions = operation.getQ_substitutions()
|
||||||
receiver = self.create_input(operation)
|
receiver = OperationSchema.create_input(self.model, operation)
|
||||||
|
|
||||||
parents: dict = {}
|
parents: dict = {}
|
||||||
children: dict = {}
|
children: dict = {}
|
||||||
|
|
|
||||||
|
|
@ -3,31 +3,17 @@
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from cctext import extract_entities
|
from apps.library.models import LibraryItem
|
||||||
from rest_framework.serializers import ValidationError
|
from apps.rsform.models import Attribution, Constituenta, CstType, OrderManager, RSFormCached
|
||||||
|
|
||||||
from apps.library.models import Editor, LibraryItem
|
|
||||||
from apps.rsform.graph import Graph
|
|
||||||
from apps.rsform.models import (
|
|
||||||
DELETED_ALIAS,
|
|
||||||
INSERT_LAST,
|
|
||||||
Constituenta,
|
|
||||||
CstType,
|
|
||||||
OrderManager,
|
|
||||||
RSFormCached,
|
|
||||||
extract_globals,
|
|
||||||
replace_entities,
|
|
||||||
replace_globals
|
|
||||||
)
|
|
||||||
|
|
||||||
from .Argument import Argument
|
from .Argument import Argument
|
||||||
from .Inheritance import Inheritance
|
from .Inheritance import Inheritance
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation
|
||||||
from .Reference import Reference
|
from .OperationSchema import OperationSchema
|
||||||
|
from .OssCache import OssCache
|
||||||
|
from .PropagationEngine import PropagationEngine
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
|
||||||
CstMapping = dict[str, Optional[Constituenta]]
|
|
||||||
CstSubstitution = list[tuple[Constituenta, Constituenta]]
|
|
||||||
|
|
||||||
|
|
||||||
class OperationSchemaCached:
|
class OperationSchemaCached:
|
||||||
|
|
@ -35,19 +21,20 @@ class OperationSchemaCached:
|
||||||
|
|
||||||
def __init__(self, model: LibraryItem):
|
def __init__(self, model: LibraryItem):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.cache = OssCache(self)
|
self.cache = OssCache(model.pk)
|
||||||
|
self.engine = PropagationEngine(self.cache)
|
||||||
|
|
||||||
def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
||||||
''' Delete Reference Operation. '''
|
''' Delete Replica Operation. '''
|
||||||
if not keep_connections:
|
if not keep_connections:
|
||||||
self.delete_operation(target, keep_constituents)
|
self.delete_operation(target, keep_constituents)
|
||||||
return
|
return
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
reference_target = self.cache.reference_target.get(target)
|
original = self.cache.replica_original.get(target)
|
||||||
if reference_target:
|
if original:
|
||||||
for arg in operation.getQ_as_argument():
|
for arg in operation.getQ_as_argument():
|
||||||
arg.argument_id = reference_target
|
arg.argument_id = original
|
||||||
arg.save()
|
arg.save()
|
||||||
self.cache.remove_operation(target)
|
self.cache.remove_operation(target)
|
||||||
operation.delete()
|
operation.delete()
|
||||||
|
|
@ -57,11 +44,11 @@ class OperationSchemaCached:
|
||||||
''' Delete Operation. '''
|
''' Delete Operation. '''
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
children = self.cache.graph.outputs[target]
|
children = self.cache.extend_graph.outputs[target]
|
||||||
if operation.result is not None and len(children) > 0:
|
if operation.result is not None and children:
|
||||||
ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
|
ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
|
||||||
if not keep_constituents:
|
if not keep_constituents:
|
||||||
self._cascade_delete_inherited(operation.pk, ids)
|
self.engine.on_delete_inherited(operation.pk, ids)
|
||||||
else:
|
else:
|
||||||
inheritance_to_delete: list[Inheritance] = []
|
inheritance_to_delete: list[Inheritance] = []
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
|
|
@ -69,7 +56,7 @@ class OperationSchemaCached:
|
||||||
child_schema = self.cache.get_schema(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
if child_schema is None:
|
if child_schema is None:
|
||||||
continue
|
continue
|
||||||
self._undo_substitutions_cst(ids, child_operation, child_schema)
|
self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
|
||||||
for item in self.cache.inheritance[child_id]:
|
for item in self.cache.inheritance[child_id]:
|
||||||
if item.parent_id in ids:
|
if item.parent_id in ids:
|
||||||
inheritance_to_delete.append(item)
|
inheritance_to_delete.append(item)
|
||||||
|
|
@ -82,7 +69,7 @@ class OperationSchemaCached:
|
||||||
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
|
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
|
||||||
''' Set input schema for operation. '''
|
''' Set input schema for operation. '''
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
has_children = len(self.cache.graph.outputs[target]) > 0
|
has_children = bool(self.cache.extend_graph.outputs[target])
|
||||||
old_schema = self.cache.get_schema(operation)
|
old_schema = self.cache.get_schema(operation)
|
||||||
if schema is None and old_schema is None or \
|
if schema is None and old_schema is None or \
|
||||||
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk):
|
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk):
|
||||||
|
|
@ -118,7 +105,7 @@ class OperationSchemaCached:
|
||||||
processed.append(current.argument)
|
processed.append(current.argument)
|
||||||
current.order = arguments.index(current.argument)
|
current.order = arguments.index(current.argument)
|
||||||
updated.append(current)
|
updated.append(current)
|
||||||
if len(deleted) > 0:
|
if deleted:
|
||||||
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)
|
||||||
|
|
@ -132,7 +119,7 @@ class OperationSchemaCached:
|
||||||
new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
|
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 added:
|
||||||
self.after_create_arguments(operation, added)
|
self.after_create_arguments(operation, added)
|
||||||
|
|
||||||
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
||||||
|
|
@ -147,14 +134,14 @@ class OperationSchemaCached:
|
||||||
x for x in substitutes
|
x for x in substitutes
|
||||||
if x['original'] == current.original and x['substitution'] == current.substitution
|
if x['original'] == current.original and x['substitution'] == current.substitution
|
||||||
]
|
]
|
||||||
if len(subs) == 0:
|
if not subs:
|
||||||
deleted.append(current)
|
deleted.append(current)
|
||||||
else:
|
else:
|
||||||
processed.append(subs[0])
|
processed.append(subs[0])
|
||||||
if len(deleted) > 0:
|
if deleted:
|
||||||
if schema is not None:
|
if schema is not None:
|
||||||
for sub in deleted:
|
for sub in deleted:
|
||||||
self._undo_substitution(schema, sub)
|
self.engine.undo_substitution(schema, sub)
|
||||||
else:
|
else:
|
||||||
for sub in deleted:
|
for sub in deleted:
|
||||||
self.cache.remove_substitution(sub)
|
self.cache.remove_substitution(sub)
|
||||||
|
|
@ -169,22 +156,7 @@ class OperationSchemaCached:
|
||||||
substitution=sub_item['substitution']
|
substitution=sub_item['substitution']
|
||||||
)
|
)
|
||||||
added.append(new_sub)
|
added.append(new_sub)
|
||||||
self._process_added_substitutions(schema, added)
|
self._on_add_substitutions(schema, added)
|
||||||
|
|
||||||
def _create_input(self, operation: Operation) -> RSFormCached:
|
|
||||||
''' Create input RSForm for given Operation. '''
|
|
||||||
schema = RSFormCached.create(
|
|
||||||
owner=self.model.owner,
|
|
||||||
alias=operation.alias,
|
|
||||||
title=operation.title,
|
|
||||||
description=operation.description,
|
|
||||||
visible=False,
|
|
||||||
access_policy=self.model.access_policy,
|
|
||||||
location=self.model.location
|
|
||||||
)
|
|
||||||
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
|
|
||||||
operation.setQ_result(schema.model)
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def execute_operation(self, operation: Operation) -> bool:
|
def execute_operation(self, operation: Operation) -> bool:
|
||||||
''' Execute target Operation. '''
|
''' Execute target Operation. '''
|
||||||
|
|
@ -197,10 +169,11 @@ class OperationSchemaCached:
|
||||||
.order_by('order')
|
.order_by('order')
|
||||||
if arg.argument.result_id is not None
|
if arg.argument.result_id is not None
|
||||||
]
|
]
|
||||||
if len(schemas) == 0:
|
if not schemas:
|
||||||
return False
|
return False
|
||||||
substitutions = operation.getQ_substitutions()
|
substitutions = operation.getQ_substitutions()
|
||||||
receiver = self._create_input(self.cache.operation_by_id[operation.pk])
|
receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk])
|
||||||
|
self.cache.insert_schema(receiver)
|
||||||
|
|
||||||
parents: dict = {}
|
parents: dict = {}
|
||||||
children: dict = {}
|
children: dict = {}
|
||||||
|
|
@ -231,7 +204,7 @@ class OperationSchemaCached:
|
||||||
receiver.reset_aliases()
|
receiver.reset_aliases()
|
||||||
receiver.resolve_all_text()
|
receiver.resolve_all_text()
|
||||||
|
|
||||||
if len(self.cache.graph.outputs[operation.pk]) > 0:
|
if self.cache.extend_graph.outputs[operation.pk]:
|
||||||
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order'))
|
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order'))
|
||||||
self.after_create_cst(receiver, receiver_items)
|
self.after_create_cst(receiver, receiver_items)
|
||||||
receiver.model.save(update_fields=['time_update'])
|
receiver.model.save(update_fields=['time_update'])
|
||||||
|
|
@ -243,7 +216,7 @@ class OperationSchemaCached:
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
self.cache.insert_schema(destination)
|
self.cache.insert_schema(destination)
|
||||||
operation = self.cache.get_operation(destination.model.pk)
|
operation = self.cache.get_operation(destination.model.pk)
|
||||||
self._undo_substitutions_cst(items, operation, destination)
|
self.engine.undo_substitutions_cst(items, operation, destination)
|
||||||
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
||||||
for item in inheritance_to_delete:
|
for item in inheritance_to_delete:
|
||||||
self.cache.remove_inheritance(item)
|
self.cache.remove_inheritance(item)
|
||||||
|
|
@ -282,37 +255,27 @@ class OperationSchemaCached:
|
||||||
exclude: Optional[list[int]] = None
|
exclude: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Trigger cascade resolutions when new Constituenta is created. '''
|
''' Trigger cascade resolutions when new Constituenta is created. '''
|
||||||
source.cache.ensure_loaded()
|
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
inserted_aliases = [cst.alias for cst in cst_list]
|
alias_mapping = create_dependant_mapping(source, cst_list)
|
||||||
depend_aliases: set[str] = set()
|
|
||||||
for new_cst in cst_list:
|
|
||||||
depend_aliases.update(new_cst.extract_references())
|
|
||||||
depend_aliases.difference_update(inserted_aliases)
|
|
||||||
alias_mapping: CstMapping = {}
|
|
||||||
for alias in depend_aliases:
|
|
||||||
cst = source.cache.by_alias.get(alias)
|
|
||||||
if cst is not None:
|
|
||||||
alias_mapping[alias] = cst
|
|
||||||
operation = self.cache.get_operation(source.model.pk)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
||||||
|
|
||||||
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
|
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
||||||
operation = self.cache.get_operation(schemaID)
|
operation = self.cache.get_operation(schemaID)
|
||||||
self._cascade_change_cst_type(operation.pk, target, new_type)
|
self.engine.on_change_cst_type(operation.pk, target, new_type)
|
||||||
|
|
||||||
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None:
|
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
operation = self.cache.get_operation(source.model.pk)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
depend_aliases = self._extract_data_references(data, old_data)
|
depend_aliases = extract_data_references(data, old_data)
|
||||||
alias_mapping: CstMapping = {}
|
alias_mapping: CstMapping = {}
|
||||||
for alias in depend_aliases:
|
for alias in depend_aliases:
|
||||||
cst = source.cache.by_alias.get(alias)
|
cst = source.cache.by_alias.get(alias)
|
||||||
if cst is not None:
|
if cst is not None:
|
||||||
alias_mapping[alias] = cst
|
alias_mapping[alias] = cst
|
||||||
self._cascade_update_cst(
|
self.engine.on_update_cst(
|
||||||
operation=operation.pk,
|
operation=operation.pk,
|
||||||
cst_id=target,
|
cst_id=target,
|
||||||
data=data,
|
data=data,
|
||||||
|
|
@ -320,15 +283,15 @@ class OperationSchemaCached:
|
||||||
mapping=alias_mapping
|
mapping=alias_mapping
|
||||||
)
|
)
|
||||||
|
|
||||||
def before_delete_cst(self, sourceID: int, target: list[int]) -> None:
|
def before_delete_cst(self, operationID: int, target: list[int]) -> None:
|
||||||
''' Trigger cascade resolutions before Constituents are deleted. '''
|
''' Trigger cascade resolutions before Constituents are deleted. '''
|
||||||
operation = self.cache.get_operation(sourceID)
|
operation = self.cache.get_operation(operationID)
|
||||||
self._cascade_delete_inherited(operation.pk, target)
|
self.engine.on_delete_inherited(operation.pk, target)
|
||||||
|
|
||||||
def before_substitute(self, schemaID: int, substitutions: CstSubstitution) -> None:
|
def before_substitute(self, schemaID: int, substitutions: CstSubstitution) -> None:
|
||||||
''' Trigger cascade resolutions before Constituents are substituted. '''
|
''' Trigger cascade resolutions before Constituents are substituted. '''
|
||||||
operation = self.cache.get_operation(schemaID)
|
operation = self.cache.get_operation(schemaID)
|
||||||
self._cascade_before_substitute(substitutions, operation)
|
self.engine.on_before_substitute(operation.pk, substitutions)
|
||||||
|
|
||||||
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||||
''' Trigger cascade resolutions before arguments are deleted. '''
|
''' Trigger cascade resolutions before arguments are deleted. '''
|
||||||
|
|
@ -337,7 +300,7 @@ class OperationSchemaCached:
|
||||||
for argument in arguments:
|
for argument in arguments:
|
||||||
parent_schema = self.cache.get_schema(argument)
|
parent_schema = self.cache.get_schema(argument)
|
||||||
if parent_schema is not None:
|
if parent_schema is not None:
|
||||||
self._execute_delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
|
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
|
||||||
|
|
||||||
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||||
''' Trigger cascade resolutions after arguments are created. '''
|
''' Trigger cascade resolutions after arguments are created. '''
|
||||||
|
|
@ -348,336 +311,27 @@ class OperationSchemaCached:
|
||||||
parent_schema = self.cache.get_schema(argument)
|
parent_schema = self.cache.get_schema(argument)
|
||||||
if parent_schema is None:
|
if parent_schema is None:
|
||||||
continue
|
continue
|
||||||
self._execute_inherit_cst(
|
self.engine.inherit_cst(
|
||||||
target_operation=target.pk,
|
target_operation=target.pk,
|
||||||
source=parent_schema,
|
source=parent_schema,
|
||||||
items=list(parent_schema.constituentsQ().order_by('order')),
|
items=list(parent_schema.constituentsQ().order_by('order')),
|
||||||
mapping={}
|
mapping={}
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
def after_create_attribution(self, schemaID: int, associations: list[Attribution],
|
||||||
def _cascade_inherit_cst(
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
self, target_operation: int,
|
''' Trigger cascade resolutions when Attribution is created. '''
|
||||||
source: RSFormCached,
|
operation = self.cache.get_operation(schemaID)
|
||||||
items: list[Constituenta],
|
self.engine.on_inherit_attribution(operation.pk, associations, exclude)
|
||||||
mapping: CstMapping,
|
|
||||||
exclude: Optional[list[int]] = None
|
|
||||||
) -> None:
|
|
||||||
children = self.cache.graph.outputs[target_operation]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
for child_id in children:
|
|
||||||
if not exclude or child_id not in exclude:
|
|
||||||
self._execute_inherit_cst(child_id, source, items, mapping)
|
|
||||||
|
|
||||||
def _execute_inherit_cst(
|
def before_delete_attribution(self, schemaID: int, associations: list[Attribution]) -> None:
|
||||||
self,
|
''' Trigger cascade resolutions when Attribution is deleted. '''
|
||||||
target_operation: int,
|
operation = self.cache.get_operation(schemaID)
|
||||||
source: RSFormCached,
|
self.engine.on_delete_attribution(operation.pk, associations)
|
||||||
items: list[Constituenta],
|
|
||||||
mapping: CstMapping
|
|
||||||
) -> None:
|
|
||||||
operation = self.cache.operation_by_id[target_operation]
|
|
||||||
destination = self.cache.get_schema(operation)
|
|
||||||
if destination is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.cache.ensure_loaded_subs()
|
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
||||||
new_mapping = self._transform_mapping(mapping, operation, destination)
|
''' Trigger cascade resolutions when Constituenta substitution is added. '''
|
||||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping)
|
if not added:
|
||||||
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
|
||||||
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
|
||||||
for index, cst in enumerate(new_cst_list):
|
|
||||||
new_inheritance = Inheritance.objects.create(
|
|
||||||
operation=operation,
|
|
||||||
child=cst,
|
|
||||||
parent=items[index]
|
|
||||||
)
|
|
||||||
self.cache.insert_inheritance(new_inheritance)
|
|
||||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
|
||||||
self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
|
|
||||||
|
|
||||||
def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
|
||||||
children = self.cache.graph.outputs[operation_id]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
self.cache.ensure_loaded_subs()
|
|
||||||
for child_id in children:
|
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
|
||||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
|
||||||
if successor_id is None:
|
|
||||||
continue
|
|
||||||
child_schema = self.cache.get_schema(child_operation)
|
|
||||||
if child_schema is None:
|
|
||||||
continue
|
|
||||||
if child_schema.change_cst_type(successor_id, ctype):
|
|
||||||
self._cascade_change_cst_type(child_id, successor_id, ctype)
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
||||||
def _cascade_update_cst(
|
|
||||||
self,
|
|
||||||
operation: int,
|
|
||||||
cst_id: int,
|
|
||||||
data: dict, old_data: dict,
|
|
||||||
mapping: CstMapping
|
|
||||||
) -> None:
|
|
||||||
children = self.cache.graph.outputs[operation]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
self.cache.ensure_loaded_subs()
|
|
||||||
for child_id in children:
|
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
|
||||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
|
||||||
if successor_id is None:
|
|
||||||
continue
|
|
||||||
child_schema = self.cache.get_schema(child_operation)
|
|
||||||
assert child_schema is not None
|
|
||||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
|
||||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping)
|
|
||||||
successor = child_schema.cache.by_id.get(successor_id)
|
|
||||||
if successor is None:
|
|
||||||
continue
|
|
||||||
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
|
|
||||||
if len(new_data) == 0:
|
|
||||||
continue
|
|
||||||
new_old_data = child_schema.update_cst(successor.pk, new_data)
|
|
||||||
if len(new_old_data) == 0:
|
|
||||||
continue
|
|
||||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
|
||||||
self._cascade_update_cst(
|
|
||||||
operation=child_id,
|
|
||||||
cst_id=successor_id,
|
|
||||||
data=new_data,
|
|
||||||
old_data=new_old_data,
|
|
||||||
mapping=new_mapping
|
|
||||||
)
|
|
||||||
|
|
||||||
def _cascade_delete_inherited(self, operation: int, target: list[int]) -> None:
|
|
||||||
children = self.cache.graph.outputs[operation]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
self.cache.ensure_loaded_subs()
|
|
||||||
for child_id in children:
|
|
||||||
self._execute_delete_inherited(child_id, target)
|
|
||||||
|
|
||||||
def _execute_delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None:
|
|
||||||
operation = self.cache.operation_by_id[operation_id]
|
|
||||||
schema = self.cache.get_schema(operation)
|
|
||||||
if schema is None:
|
|
||||||
return
|
|
||||||
self._undo_substitutions_cst(parent_ids, operation, schema)
|
|
||||||
target_ids = self.cache.get_inheritors_list(parent_ids, operation_id)
|
|
||||||
self._cascade_delete_inherited(operation_id, target_ids)
|
|
||||||
if len(target_ids) > 0:
|
|
||||||
self.cache.remove_cst(operation_id, target_ids)
|
|
||||||
schema.delete_cst(target_ids)
|
|
||||||
|
|
||||||
def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
|
|
||||||
children = self.cache.graph.outputs[operation.pk]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
self.cache.ensure_loaded_subs()
|
|
||||||
for child_id in children:
|
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
|
||||||
child_schema = self.cache.get_schema(child_operation)
|
|
||||||
if child_schema is None:
|
|
||||||
continue
|
|
||||||
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
|
||||||
if len(new_substitutions) == 0:
|
|
||||||
continue
|
|
||||||
self._cascade_before_substitute(new_substitutions, child_operation)
|
|
||||||
child_schema.substitute(new_substitutions)
|
|
||||||
|
|
||||||
def _cascade_partial_mapping(
|
|
||||||
self,
|
|
||||||
mapping: CstMapping,
|
|
||||||
target: list[int],
|
|
||||||
operation: int,
|
|
||||||
schema: RSFormCached
|
|
||||||
) -> None:
|
|
||||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(mapping)
|
|
||||||
schema.apply_partial_mapping(alias_mapping, target)
|
|
||||||
children = self.cache.graph.outputs[operation]
|
|
||||||
if len(children) == 0:
|
|
||||||
return
|
|
||||||
self.cache.ensure_loaded_subs()
|
|
||||||
for child_id in children:
|
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
|
||||||
child_schema = self.cache.get_schema(child_operation)
|
|
||||||
if child_schema is None:
|
|
||||||
continue
|
|
||||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
|
||||||
if not new_mapping:
|
|
||||||
continue
|
|
||||||
new_target = self.cache.get_inheritors_list(target, child_id)
|
|
||||||
if len(new_target) == 0:
|
|
||||||
continue
|
|
||||||
self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]:
|
|
||||||
result: dict[str, str] = {}
|
|
||||||
for alias, cst in mapping.items():
|
|
||||||
if cst is None:
|
|
||||||
result[alias] = DELETED_ALIAS
|
|
||||||
else:
|
|
||||||
result[alias] = cst.alias
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping:
|
|
||||||
if len(mapping) == 0:
|
|
||||||
return mapping
|
|
||||||
result: CstMapping = {}
|
|
||||||
for alias, cst in mapping.items():
|
|
||||||
if cst is None:
|
|
||||||
result[alias] = None
|
|
||||||
continue
|
|
||||||
successor_id = self.cache.get_successor(cst.pk, operation.pk)
|
|
||||||
if successor_id is None:
|
|
||||||
continue
|
|
||||||
successor = schema.cache.by_id.get(successor_id)
|
|
||||||
if successor is None:
|
|
||||||
continue
|
|
||||||
result[alias] = successor
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _determine_insert_position(
|
|
||||||
self, prototype_id: int,
|
|
||||||
operation: Operation,
|
|
||||||
source: RSFormCached,
|
|
||||||
destination: RSFormCached
|
|
||||||
) -> int:
|
|
||||||
''' Determine insert_after for new constituenta. '''
|
|
||||||
prototype = source.cache.by_id[prototype_id]
|
|
||||||
prototype_index = source.cache.constituents.index(prototype)
|
|
||||||
if prototype_index == 0:
|
|
||||||
return 0
|
|
||||||
prev_cst = source.cache.constituents[prototype_index - 1]
|
|
||||||
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
|
||||||
if inherited_prev_id is None:
|
|
||||||
return INSERT_LAST
|
|
||||||
prev_cst = destination.cache.by_id[inherited_prev_id]
|
|
||||||
prev_index = destination.cache.constituents.index(prev_cst)
|
|
||||||
return prev_index + 1
|
|
||||||
|
|
||||||
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
|
|
||||||
result: set[str] = set()
|
|
||||||
if 'definition_formal' in data:
|
|
||||||
result.update(extract_globals(data['definition_formal']))
|
|
||||||
result.update(extract_globals(old_data['definition_formal']))
|
|
||||||
if 'term_raw' in data:
|
|
||||||
result.update(extract_entities(data['term_raw']))
|
|
||||||
result.update(extract_entities(old_data['term_raw']))
|
|
||||||
if 'definition_raw' in data:
|
|
||||||
result.update(extract_entities(data['definition_raw']))
|
|
||||||
result.update(extract_entities(old_data['definition_raw']))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
|
|
||||||
new_data = {}
|
|
||||||
if 'term_forms' in data:
|
|
||||||
if old_data['term_forms'] == cst.term_forms:
|
|
||||||
new_data['term_forms'] = data['term_forms']
|
|
||||||
if 'convention' in data:
|
|
||||||
new_data['convention'] = data['convention']
|
|
||||||
if 'definition_formal' in data:
|
|
||||||
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
|
|
||||||
if 'term_raw' in data:
|
|
||||||
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
|
|
||||||
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
|
|
||||||
if 'definition_raw' in data:
|
|
||||||
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
|
|
||||||
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
|
|
||||||
return new_data
|
|
||||||
|
|
||||||
def _transform_substitutions(
|
|
||||||
self,
|
|
||||||
target: CstSubstitution,
|
|
||||||
operation: int,
|
|
||||||
schema: RSFormCached
|
|
||||||
) -> CstSubstitution:
|
|
||||||
result: CstSubstitution = []
|
|
||||||
for current_sub in target:
|
|
||||||
sub_replaced = False
|
|
||||||
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation)
|
|
||||||
if new_substitution_id is None:
|
|
||||||
for sub in self.cache.substitutions[operation]:
|
|
||||||
if sub.original_id == current_sub[1].pk:
|
|
||||||
sub_replaced = True
|
|
||||||
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation)
|
|
||||||
break
|
|
||||||
|
|
||||||
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation)
|
|
||||||
original_replaced = False
|
|
||||||
if new_original_id is None:
|
|
||||||
for sub in self.cache.substitutions[operation]:
|
|
||||||
if sub.original_id == current_sub[0].pk:
|
|
||||||
original_replaced = True
|
|
||||||
sub.original_id = current_sub[1].pk
|
|
||||||
sub.save()
|
|
||||||
new_original_id = new_substitution_id
|
|
||||||
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation)
|
|
||||||
break
|
|
||||||
|
|
||||||
if sub_replaced and original_replaced:
|
|
||||||
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
|
|
||||||
|
|
||||||
for sub in self.cache.substitutions[operation]:
|
|
||||||
if sub.substitution_id == current_sub[0].pk:
|
|
||||||
sub.substitution_id = current_sub[1].pk
|
|
||||||
sub.save()
|
|
||||||
|
|
||||||
if new_original_id is not None and new_substitution_id is not None:
|
|
||||||
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
|
|
||||||
to_process = []
|
|
||||||
for sub in self.cache.substitutions[operation.pk]:
|
|
||||||
if sub.original_id in target_ids or sub.substitution_id in target_ids:
|
|
||||||
to_process.append(sub)
|
|
||||||
for sub in to_process:
|
|
||||||
self._undo_substitution(schema, sub, target_ids)
|
|
||||||
|
|
||||||
def _undo_substitution(
|
|
||||||
self,
|
|
||||||
schema: RSFormCached,
|
|
||||||
target: Substitution,
|
|
||||||
ignore_parents: Optional[list[int]] = None
|
|
||||||
) -> None:
|
|
||||||
if ignore_parents is None:
|
|
||||||
ignore_parents = []
|
|
||||||
operation_id = target.operation_id
|
|
||||||
original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target)
|
|
||||||
|
|
||||||
dependant = []
|
|
||||||
for cst_id in original_schema.get_dependant([original_cst.pk]):
|
|
||||||
if cst_id not in ignore_parents:
|
|
||||||
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
|
||||||
if inheritor_id is not None:
|
|
||||||
dependant.append(inheritor_id)
|
|
||||||
|
|
||||||
self.cache.substitutions[operation_id].remove(target)
|
|
||||||
target.delete()
|
|
||||||
|
|
||||||
new_original: Optional[Constituenta] = None
|
|
||||||
if original_cst.pk not in ignore_parents:
|
|
||||||
full_cst = Constituenta.objects.get(pk=original_cst.pk)
|
|
||||||
self.after_create_cst(original_schema, [full_cst])
|
|
||||||
new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id)
|
|
||||||
assert new_original_id is not None
|
|
||||||
new_original = schema.cache.by_id[new_original_id]
|
|
||||||
if len(dependant) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id)
|
|
||||||
assert substitution_id is not None
|
|
||||||
substitution_inheritor = schema.cache.by_id[substitution_id]
|
|
||||||
mapping = {substitution_inheritor.alias: new_original}
|
|
||||||
self._cascade_partial_mapping(mapping, dependant, operation_id, schema)
|
|
||||||
|
|
||||||
def _process_added_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
|
||||||
if len(added) == 0:
|
|
||||||
return
|
return
|
||||||
if schema is None:
|
if schema is None:
|
||||||
for sub in added:
|
for sub in added:
|
||||||
|
|
@ -697,185 +351,3 @@ class OperationSchemaCached:
|
||||||
schema.substitute(cst_mapping)
|
schema.substitute(cst_mapping)
|
||||||
for sub in added:
|
for sub in added:
|
||||||
self.cache.insert_substitution(sub)
|
self.cache.insert_substitution(sub)
|
||||||
|
|
||||||
|
|
||||||
class OssCache:
|
|
||||||
''' Cache for OSS data. '''
|
|
||||||
|
|
||||||
def __init__(self, oss: OperationSchemaCached):
|
|
||||||
self._oss = oss
|
|
||||||
self._schemas: list[RSFormCached] = []
|
|
||||||
self._schema_by_id: dict[int, RSFormCached] = {}
|
|
||||||
|
|
||||||
self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id', 'operation_type'))
|
|
||||||
self.operation_by_id = {operation.pk: operation for operation in self.operations}
|
|
||||||
self.graph = Graph[int]()
|
|
||||||
for operation in self.operations:
|
|
||||||
self.graph.add_node(operation.pk)
|
|
||||||
|
|
||||||
references = Reference.objects.filter(reference__oss=self._oss.model).only('reference_id', 'target_id')
|
|
||||||
self.reference_target = {ref.reference_id: ref.target_id for ref in references}
|
|
||||||
arguments = Argument.objects \
|
|
||||||
.filter(operation__oss=self._oss.model) \
|
|
||||||
.only('operation_id', 'argument_id') \
|
|
||||||
.order_by('order')
|
|
||||||
for argument in arguments:
|
|
||||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
|
||||||
target = self.reference_target.get(argument.argument_id)
|
|
||||||
if target is not None:
|
|
||||||
self.graph.add_edge(target, argument.operation_id)
|
|
||||||
|
|
||||||
self.is_loaded_subs = False
|
|
||||||
self.substitutions: dict[int, list[Substitution]] = {}
|
|
||||||
self.inheritance: dict[int, list[Inheritance]] = {}
|
|
||||||
|
|
||||||
def ensure_loaded_subs(self) -> None:
|
|
||||||
''' Ensure cache is fully loaded. '''
|
|
||||||
if self.is_loaded_subs:
|
|
||||||
return
|
|
||||||
self.is_loaded_subs = True
|
|
||||||
for operation in self.operations:
|
|
||||||
self.inheritance[operation.pk] = []
|
|
||||||
self.substitutions[operation.pk] = []
|
|
||||||
for sub in Substitution.objects.filter(operation__oss=self._oss.model).only(
|
|
||||||
'operation_id', 'original_id', 'substitution_id'):
|
|
||||||
self.substitutions[sub.operation_id].append(sub)
|
|
||||||
for item in Inheritance.objects.filter(operation__oss=self._oss.model).only(
|
|
||||||
'operation_id', 'parent_id', 'child_id'):
|
|
||||||
self.inheritance[item.operation_id].append(item)
|
|
||||||
|
|
||||||
def get_schema(self, operation: Operation) -> Optional[RSFormCached]:
|
|
||||||
''' Get schema by Operation. '''
|
|
||||||
if operation.result_id is None:
|
|
||||||
return None
|
|
||||||
if operation.result_id in self._schema_by_id:
|
|
||||||
return self._schema_by_id[operation.result_id]
|
|
||||||
else:
|
|
||||||
schema = RSFormCached.from_id(operation.result_id)
|
|
||||||
schema.cache.ensure_loaded()
|
|
||||||
self._insert_new(schema)
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def get_operation(self, schemaID: int) -> Operation:
|
|
||||||
''' Get operation by schema. '''
|
|
||||||
for operation in self.operations:
|
|
||||||
if operation.result_id == schemaID and operation.operation_type != OperationType.REFERENCE:
|
|
||||||
return operation
|
|
||||||
raise ValueError(f'Operation for schema {schemaID} not found')
|
|
||||||
|
|
||||||
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
|
|
||||||
''' Get child for parent inside target RSFrom. '''
|
|
||||||
for item in self.inheritance[operation]:
|
|
||||||
if item.parent_id == parent_cst:
|
|
||||||
return item.child_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_inheritors_list(self, target: list[int], operation: int) -> list[int]:
|
|
||||||
''' Get child for parent inside target RSFrom. '''
|
|
||||||
result = []
|
|
||||||
for item in self.inheritance[operation]:
|
|
||||||
if item.parent_id in target:
|
|
||||||
result.append(item.child_id)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
|
|
||||||
''' Get child for parent inside target RSFrom including substitutions. '''
|
|
||||||
for sub in self.substitutions[operation]:
|
|
||||||
if sub.original_id == parent_cst:
|
|
||||||
return self.get_inheritor(sub.substitution_id, operation)
|
|
||||||
return self.get_inheritor(parent_cst, operation)
|
|
||||||
|
|
||||||
def insert_schema(self, schema: RSFormCached) -> None:
|
|
||||||
''' Insert new schema. '''
|
|
||||||
if not self._schema_by_id.get(schema.model.pk):
|
|
||||||
schema.cache.ensure_loaded()
|
|
||||||
self._insert_new(schema)
|
|
||||||
|
|
||||||
def insert_argument(self, argument: Argument) -> None:
|
|
||||||
''' Insert new argument. '''
|
|
||||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
|
||||||
target = self.reference_target.get(argument.argument_id)
|
|
||||||
if target is not None:
|
|
||||||
self.graph.add_edge(target, argument.operation_id)
|
|
||||||
|
|
||||||
def insert_inheritance(self, inheritance: Inheritance) -> None:
|
|
||||||
''' Insert new inheritance. '''
|
|
||||||
self.inheritance[inheritance.operation_id].append(inheritance)
|
|
||||||
|
|
||||||
def insert_substitution(self, sub: Substitution) -> None:
|
|
||||||
''' Insert new substitution. '''
|
|
||||||
self.substitutions[sub.operation_id].append(sub)
|
|
||||||
|
|
||||||
def remove_cst(self, operation: int, target: list[int]) -> None:
|
|
||||||
''' Remove constituents from operation. '''
|
|
||||||
subs_to_delete = [
|
|
||||||
sub for sub in self.substitutions[operation]
|
|
||||||
if sub.original_id in target or sub.substitution_id in target
|
|
||||||
]
|
|
||||||
for sub in subs_to_delete:
|
|
||||||
self.substitutions[operation].remove(sub)
|
|
||||||
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
|
|
||||||
for item in inherit_to_delete:
|
|
||||||
self.inheritance[operation].remove(item)
|
|
||||||
|
|
||||||
def remove_schema(self, schema: RSFormCached) -> None:
|
|
||||||
''' Remove schema from cache. '''
|
|
||||||
self._schemas.remove(schema)
|
|
||||||
del self._schema_by_id[schema.model.pk]
|
|
||||||
|
|
||||||
def remove_operation(self, operation: int) -> None:
|
|
||||||
''' Remove operation from cache. '''
|
|
||||||
target = self.operation_by_id[operation]
|
|
||||||
self.graph.remove_node(operation)
|
|
||||||
if target.result_id in self._schema_by_id:
|
|
||||||
self._schemas.remove(self._schema_by_id[target.result_id])
|
|
||||||
del self._schema_by_id[target.result_id]
|
|
||||||
self.operations.remove(self.operation_by_id[operation])
|
|
||||||
del self.operation_by_id[operation]
|
|
||||||
if operation in self.reference_target:
|
|
||||||
del self.reference_target[operation]
|
|
||||||
if self.is_loaded_subs:
|
|
||||||
del self.substitutions[operation]
|
|
||||||
del self.inheritance[operation]
|
|
||||||
|
|
||||||
def remove_argument(self, argument: Argument) -> None:
|
|
||||||
''' Remove argument from cache. '''
|
|
||||||
self.graph.remove_edge(argument.argument_id, argument.operation_id)
|
|
||||||
target = self.reference_target.get(argument.argument_id)
|
|
||||||
if target is not None:
|
|
||||||
if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
|
|
||||||
self.graph.remove_edge(target, argument.operation_id)
|
|
||||||
|
|
||||||
def remove_substitution(self, target: Substitution) -> None:
|
|
||||||
''' Remove substitution from cache. '''
|
|
||||||
self.substitutions[target.operation_id].remove(target)
|
|
||||||
|
|
||||||
def remove_inheritance(self, target: Inheritance) -> None:
|
|
||||||
''' Remove inheritance from cache. '''
|
|
||||||
self.inheritance[target.operation_id].remove(target)
|
|
||||||
|
|
||||||
def unfold_sub(self, sub: Substitution) -> tuple[RSFormCached, RSFormCached, Constituenta, Constituenta]:
|
|
||||||
''' Unfold substitution into original and substitution forms. '''
|
|
||||||
operation = self.operation_by_id[sub.operation_id]
|
|
||||||
parents = self.graph.inputs[operation.pk]
|
|
||||||
original_cst = None
|
|
||||||
substitution_cst = None
|
|
||||||
original_schema = None
|
|
||||||
substitution_schema = None
|
|
||||||
for parent_id in parents:
|
|
||||||
parent_schema = self.get_schema(self.operation_by_id[parent_id])
|
|
||||||
if parent_schema is None:
|
|
||||||
continue
|
|
||||||
if sub.original_id in parent_schema.cache.by_id:
|
|
||||||
original_schema = parent_schema
|
|
||||||
original_cst = original_schema.cache.by_id[sub.original_id]
|
|
||||||
if sub.substitution_id in parent_schema.cache.by_id:
|
|
||||||
substitution_schema = parent_schema
|
|
||||||
substitution_cst = substitution_schema.cache.by_id[sub.substitution_id]
|
|
||||||
if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None:
|
|
||||||
raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.')
|
|
||||||
return original_schema, substitution_schema, original_cst, substitution_cst
|
|
||||||
|
|
||||||
def _insert_new(self, schema: RSFormCached) -> None:
|
|
||||||
self._schemas.append(schema)
|
|
||||||
self._schema_by_id[schema.model.pk] = schema
|
|
||||||
|
|
|
||||||
188
rsconcept/backend/apps/oss/models/OssCache.py
Normal file
188
rsconcept/backend/apps/oss/models/OssCache.py
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
''' Models: OSS API. '''
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from apps.rsform.graph import Graph
|
||||||
|
from apps.rsform.models import RSFormCached
|
||||||
|
|
||||||
|
from .Argument import Argument
|
||||||
|
from .Inheritance import Inheritance
|
||||||
|
from .Operation import Operation, OperationType
|
||||||
|
from .Replica import Replica
|
||||||
|
from .Substitution import Substitution
|
||||||
|
|
||||||
|
|
||||||
|
class OssCache:
|
||||||
|
''' Cache for OSS data. '''
|
||||||
|
|
||||||
|
def __init__(self, item_id: int):
|
||||||
|
self._item_id = item_id
|
||||||
|
self._schemas: list[RSFormCached] = []
|
||||||
|
self._schema_by_id: dict[int, RSFormCached] = {}
|
||||||
|
|
||||||
|
self.operations = list(Operation.objects.filter(oss_id=item_id).only('result_id', 'operation_type'))
|
||||||
|
self.operation_by_id = {operation.pk: operation for operation in self.operations}
|
||||||
|
self.graph = Graph[int]()
|
||||||
|
self.extend_graph = Graph[int]()
|
||||||
|
for operation in self.operations:
|
||||||
|
self.graph.add_node(operation.pk)
|
||||||
|
self.extend_graph.add_node(operation.pk)
|
||||||
|
|
||||||
|
replicas = Replica.objects.filter(replica__oss_id=self._item_id).only('replica_id', 'original_id')
|
||||||
|
self.replica_original = {rep.replica_id: rep.original_id for rep in replicas}
|
||||||
|
arguments = Argument.objects \
|
||||||
|
.filter(operation__oss_id=self._item_id) \
|
||||||
|
.only('operation_id', 'argument_id') \
|
||||||
|
.order_by('order')
|
||||||
|
for argument in arguments:
|
||||||
|
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||||
|
self.extend_graph.add_edge(argument.argument_id, argument.operation_id)
|
||||||
|
original = self.replica_original.get(argument.argument_id)
|
||||||
|
if original is not None:
|
||||||
|
self.extend_graph.add_edge(original, argument.operation_id)
|
||||||
|
|
||||||
|
self.is_loaded_subs = False
|
||||||
|
self.substitutions: dict[int, list[Substitution]] = {}
|
||||||
|
self.inheritance: dict[int, list[Inheritance]] = {}
|
||||||
|
|
||||||
|
def ensure_loaded_subs(self) -> None:
|
||||||
|
''' Ensure cache is fully loaded. '''
|
||||||
|
if self.is_loaded_subs:
|
||||||
|
return
|
||||||
|
self.is_loaded_subs = True
|
||||||
|
for operation in self.operations:
|
||||||
|
self.inheritance[operation.pk] = []
|
||||||
|
self.substitutions[operation.pk] = []
|
||||||
|
for sub in Substitution.objects.filter(operation__oss_id=self._item_id).only(
|
||||||
|
'operation_id', 'original_id', 'substitution_id', 'original__schema_id'):
|
||||||
|
self.substitutions[sub.operation_id].append(sub)
|
||||||
|
for item in Inheritance.objects.filter(operation__oss_id=self._item_id).only(
|
||||||
|
'operation_id', 'parent_id', 'child_id'):
|
||||||
|
self.inheritance[item.operation_id].append(item)
|
||||||
|
|
||||||
|
def get_schema(self, operation: Operation) -> Optional[RSFormCached]:
|
||||||
|
''' Get schema by Operation. '''
|
||||||
|
if operation.result_id is None:
|
||||||
|
return None
|
||||||
|
if operation.result_id in self._schema_by_id:
|
||||||
|
return self._schema_by_id[operation.result_id]
|
||||||
|
else:
|
||||||
|
schema = RSFormCached.from_id(operation.result_id)
|
||||||
|
schema.cache.ensure_loaded()
|
||||||
|
self._insert_new(schema)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_schema_by_id(self, target: int) -> RSFormCached:
|
||||||
|
''' Get schema by Operation. '''
|
||||||
|
if target in self._schema_by_id:
|
||||||
|
return self._schema_by_id[target]
|
||||||
|
else:
|
||||||
|
schema = RSFormCached.from_id(target)
|
||||||
|
schema.cache.ensure_loaded()
|
||||||
|
self._insert_new(schema)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_operation(self, schemaID: int) -> Operation:
|
||||||
|
''' Get operation by schema. '''
|
||||||
|
for operation in self.operations:
|
||||||
|
if operation.result_id == schemaID and operation.operation_type != OperationType.REPLICA:
|
||||||
|
return operation
|
||||||
|
raise ValueError(f'Operation for schema {schemaID} not found')
|
||||||
|
|
||||||
|
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||||
|
''' Get child for parent inside target RSFrom. '''
|
||||||
|
for item in self.inheritance[operation]:
|
||||||
|
if item.parent_id == parent_cst:
|
||||||
|
return item.child_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_inheritors_list(self, target: list[int], operation: int) -> list[int]:
|
||||||
|
''' Get child for parent inside target RSFrom. '''
|
||||||
|
result = []
|
||||||
|
for item in self.inheritance[operation]:
|
||||||
|
if item.parent_id in target:
|
||||||
|
result.append(item.child_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||||
|
''' Get child for parent inside target RSFrom including substitutions. '''
|
||||||
|
for sub in self.substitutions[operation]:
|
||||||
|
if sub.original_id == parent_cst:
|
||||||
|
return self.get_inheritor(sub.substitution_id, operation)
|
||||||
|
return self.get_inheritor(parent_cst, operation)
|
||||||
|
|
||||||
|
def insert_schema(self, schema: RSFormCached) -> None:
|
||||||
|
''' Insert new schema. '''
|
||||||
|
if not self._schema_by_id.get(schema.model.pk):
|
||||||
|
schema.cache.ensure_loaded()
|
||||||
|
self._insert_new(schema)
|
||||||
|
|
||||||
|
def insert_argument(self, argument: Argument) -> None:
|
||||||
|
''' Insert new argument. '''
|
||||||
|
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||||
|
self.extend_graph.add_edge(argument.argument_id, argument.operation_id)
|
||||||
|
target = self.replica_original.get(argument.argument_id)
|
||||||
|
if target is not None:
|
||||||
|
self.extend_graph.add_edge(target, argument.operation_id)
|
||||||
|
|
||||||
|
def insert_inheritance(self, inheritance: Inheritance) -> None:
|
||||||
|
''' Insert new inheritance. '''
|
||||||
|
self.inheritance[inheritance.operation_id].append(inheritance)
|
||||||
|
|
||||||
|
def insert_substitution(self, sub: Substitution) -> None:
|
||||||
|
''' Insert new substitution. '''
|
||||||
|
self.substitutions[sub.operation_id].append(sub)
|
||||||
|
|
||||||
|
def remove_cst(self, operation: int, target: list[int]) -> None:
|
||||||
|
''' Remove constituents from operation. '''
|
||||||
|
subs_to_delete = [
|
||||||
|
sub for sub in self.substitutions[operation]
|
||||||
|
if sub.original_id in target or sub.substitution_id in target
|
||||||
|
]
|
||||||
|
for sub in subs_to_delete:
|
||||||
|
self.substitutions[operation].remove(sub)
|
||||||
|
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
|
||||||
|
for item in inherit_to_delete:
|
||||||
|
self.inheritance[operation].remove(item)
|
||||||
|
|
||||||
|
def remove_schema(self, schema: RSFormCached) -> None:
|
||||||
|
''' Remove schema from cache. '''
|
||||||
|
self._schemas.remove(schema)
|
||||||
|
del self._schema_by_id[schema.model.pk]
|
||||||
|
|
||||||
|
def remove_operation(self, operation: int) -> None:
|
||||||
|
''' Remove operation from cache. '''
|
||||||
|
target = self.operation_by_id[operation]
|
||||||
|
self.graph.remove_node(operation)
|
||||||
|
self.extend_graph.remove_node(operation)
|
||||||
|
if target.result_id in self._schema_by_id:
|
||||||
|
self._schemas.remove(self._schema_by_id[target.result_id])
|
||||||
|
del self._schema_by_id[target.result_id]
|
||||||
|
self.operations.remove(self.operation_by_id[operation])
|
||||||
|
del self.operation_by_id[operation]
|
||||||
|
if operation in self.replica_original:
|
||||||
|
del self.replica_original[operation]
|
||||||
|
if self.is_loaded_subs:
|
||||||
|
del self.substitutions[operation]
|
||||||
|
del self.inheritance[operation]
|
||||||
|
|
||||||
|
def remove_argument(self, argument: Argument) -> None:
|
||||||
|
''' Remove argument from cache. '''
|
||||||
|
self.graph.remove_edge(argument.argument_id, argument.operation_id)
|
||||||
|
self.extend_graph.remove_edge(argument.argument_id, argument.operation_id)
|
||||||
|
target = self.replica_original.get(argument.argument_id)
|
||||||
|
if target is not None:
|
||||||
|
if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
|
||||||
|
self.extend_graph.remove_edge(target, argument.operation_id)
|
||||||
|
|
||||||
|
def remove_substitution(self, target: Substitution) -> None:
|
||||||
|
''' Remove substitution from cache. '''
|
||||||
|
self.substitutions[target.operation_id].remove(target)
|
||||||
|
|
||||||
|
def remove_inheritance(self, target: Inheritance) -> None:
|
||||||
|
''' Remove inheritance from cache. '''
|
||||||
|
self.inheritance[target.operation_id].remove(target)
|
||||||
|
|
||||||
|
def _insert_new(self, schema: RSFormCached) -> None:
|
||||||
|
self._schemas.append(schema)
|
||||||
|
self._schema_by_id[schema.model.pk] = schema
|
||||||
386
rsconcept/backend/apps/oss/models/PropagationEngine.py
Normal file
386
rsconcept/backend/apps/oss/models/PropagationEngine.py
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
''' Models: Change propagation engine. '''
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
from apps.rsform.models import INSERT_LAST, Attribution, Constituenta, CstType, RSFormCached
|
||||||
|
|
||||||
|
from .Inheritance import Inheritance
|
||||||
|
from .Operation import Operation
|
||||||
|
from .OssCache import OssCache
|
||||||
|
from .Substitution import Substitution
|
||||||
|
from .utils import (
|
||||||
|
CstMapping,
|
||||||
|
CstSubstitution,
|
||||||
|
create_dependant_mapping,
|
||||||
|
cst_mapping_to_alias,
|
||||||
|
map_cst_update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PropagationEngine:
|
||||||
|
''' OSS changes propagation engine. '''
|
||||||
|
|
||||||
|
def __init__(self, cache: OssCache):
|
||||||
|
self.cache = cache
|
||||||
|
|
||||||
|
def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operation_id]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||||
|
if successor_id is None:
|
||||||
|
continue
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is None:
|
||||||
|
continue
|
||||||
|
if child_schema.change_cst_type(successor_id, ctype):
|
||||||
|
self.on_change_cst_type(child_id, successor_id, ctype)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||||
|
def on_inherit_cst(
|
||||||
|
self,
|
||||||
|
target_operation: int,
|
||||||
|
source: RSFormCached,
|
||||||
|
items: list[Constituenta],
|
||||||
|
mapping: CstMapping,
|
||||||
|
exclude: Optional[list[int]] = None
|
||||||
|
) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituenta is inherited. '''
|
||||||
|
children = self.cache.extend_graph.outputs[target_operation]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
for child_id in children:
|
||||||
|
if not exclude or child_id not in exclude:
|
||||||
|
self.inherit_cst(child_id, source, items, mapping)
|
||||||
|
|
||||||
|
def inherit_cst(
|
||||||
|
self,
|
||||||
|
target_operation: int,
|
||||||
|
source: RSFormCached,
|
||||||
|
items: list[Constituenta],
|
||||||
|
mapping: CstMapping
|
||||||
|
) -> None:
|
||||||
|
''' Execute inheritance of Constituenta. '''
|
||||||
|
operation = self.cache.operation_by_id[target_operation]
|
||||||
|
destination = self.cache.get_schema(operation)
|
||||||
|
if destination is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
new_mapping = self._transform_mapping(mapping, operation, destination)
|
||||||
|
alias_mapping = cst_mapping_to_alias(new_mapping)
|
||||||
|
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
||||||
|
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
||||||
|
for index, cst in enumerate(new_cst_list):
|
||||||
|
new_inheritance = Inheritance.objects.create(
|
||||||
|
operation=operation,
|
||||||
|
child=cst,
|
||||||
|
parent=items[index]
|
||||||
|
)
|
||||||
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
|
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||||
|
self.on_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||||
|
def on_update_cst(
|
||||||
|
self,
|
||||||
|
operation: int,
|
||||||
|
cst_id: int,
|
||||||
|
data: dict, old_data: dict,
|
||||||
|
mapping: CstMapping
|
||||||
|
) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operation]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||||
|
if successor_id is None:
|
||||||
|
continue
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
assert child_schema is not None
|
||||||
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
|
alias_mapping = cst_mapping_to_alias(new_mapping)
|
||||||
|
successor = child_schema.cache.by_id.get(successor_id)
|
||||||
|
if successor is None:
|
||||||
|
continue
|
||||||
|
new_data = map_cst_update_data(successor, data, old_data, alias_mapping)
|
||||||
|
if not new_data:
|
||||||
|
continue
|
||||||
|
new_old_data = child_schema.update_cst(successor.pk, new_data)
|
||||||
|
if not new_old_data:
|
||||||
|
continue
|
||||||
|
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||||
|
self.on_update_cst(
|
||||||
|
operation=child_id,
|
||||||
|
cst_id=successor_id,
|
||||||
|
data=new_data,
|
||||||
|
old_data=new_old_data,
|
||||||
|
mapping=new_mapping
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_inherit_attribution(self, operationID: int,
|
||||||
|
items: list[Attribution],
|
||||||
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
|
''' Trigger cascade resolutions when Attribution is inherited. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operationID]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
for child_id in children:
|
||||||
|
if not exclude or child_id not in exclude:
|
||||||
|
self.inherit_association(child_id, items)
|
||||||
|
|
||||||
|
def inherit_association(self, target: int, items: list[Attribution]) -> None:
|
||||||
|
''' Execute inheritance of Associations. '''
|
||||||
|
operation = self.cache.operation_by_id[target]
|
||||||
|
if operation.result is None or not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
|
||||||
|
existing_associations = set(
|
||||||
|
Attribution.objects.filter(
|
||||||
|
container__schema_id=operation.result_id,
|
||||||
|
).values_list('container_id', 'attribute_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
new_associations: list[Attribution] = []
|
||||||
|
for assoc in items:
|
||||||
|
new_container = self.cache.get_inheritor(assoc.container_id, target)
|
||||||
|
new_attribute = self.cache.get_inheritor(assoc.attribute_id, target)
|
||||||
|
if new_container is None or new_attribute is None \
|
||||||
|
or new_attribute == new_container \
|
||||||
|
or (new_container, new_attribute) in existing_associations:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_associations.append(Attribution(
|
||||||
|
container_id=new_container,
|
||||||
|
attribute_id=new_attribute
|
||||||
|
))
|
||||||
|
if new_associations:
|
||||||
|
new_associations = Attribution.objects.bulk_create(new_associations)
|
||||||
|
self.on_inherit_attribution(target, new_associations)
|
||||||
|
|
||||||
|
def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituenta substitution is executed. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operationID]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is None:
|
||||||
|
continue
|
||||||
|
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
||||||
|
if not new_substitutions:
|
||||||
|
continue
|
||||||
|
self.on_before_substitute(child_operation.pk, new_substitutions)
|
||||||
|
child_schema.substitute(new_substitutions)
|
||||||
|
|
||||||
|
def on_delete_attribution(self, operationID: int, associations: list[Attribution]) -> None:
|
||||||
|
''' Trigger cascade resolutions when Attribution is deleted. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operationID]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
deleted: list[Attribution] = []
|
||||||
|
for attr in associations:
|
||||||
|
new_container = self.cache.get_inheritor(attr.container_id, child_id)
|
||||||
|
new_attribute = self.cache.get_inheritor(attr.attribute_id, child_id)
|
||||||
|
if new_container is None or new_attribute is None:
|
||||||
|
continue
|
||||||
|
deleted_assoc = Attribution.objects.filter(
|
||||||
|
container=new_container,
|
||||||
|
attribute=new_attribute
|
||||||
|
)
|
||||||
|
if deleted_assoc.exists():
|
||||||
|
deleted.append(deleted_assoc[0])
|
||||||
|
if deleted:
|
||||||
|
self.on_delete_attribution(child_id, deleted)
|
||||||
|
Attribution.objects.filter(pk__in=[assoc.pk for assoc in deleted]).delete()
|
||||||
|
|
||||||
|
def on_delete_inherited(self, operation: int, target: list[int]) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
|
||||||
|
children = self.cache.extend_graph.outputs[operation]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
self.delete_inherited(child_id, target)
|
||||||
|
|
||||||
|
def delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None:
|
||||||
|
''' Execute deletion of Constituenta inheritance. '''
|
||||||
|
operation = self.cache.operation_by_id[operation_id]
|
||||||
|
schema = self.cache.get_schema(operation)
|
||||||
|
if schema is None:
|
||||||
|
return
|
||||||
|
self.undo_substitutions_cst(parent_ids, operation, schema)
|
||||||
|
target_ids = self.cache.get_inheritors_list(parent_ids, operation_id)
|
||||||
|
self.on_delete_inherited(operation_id, target_ids)
|
||||||
|
if target_ids:
|
||||||
|
self.cache.remove_cst(operation_id, target_ids)
|
||||||
|
schema.delete_cst(target_ids)
|
||||||
|
|
||||||
|
def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
|
||||||
|
''' Undo substitutions for Constituents. '''
|
||||||
|
to_process = []
|
||||||
|
for sub in self.cache.substitutions[operation.pk]:
|
||||||
|
if sub.original_id in target_ids or sub.substitution_id in target_ids:
|
||||||
|
to_process.append(sub)
|
||||||
|
for sub in to_process:
|
||||||
|
self.undo_substitution(schema, sub, target_ids)
|
||||||
|
|
||||||
|
def undo_substitution(
|
||||||
|
self,
|
||||||
|
schema: RSFormCached,
|
||||||
|
target: Substitution,
|
||||||
|
ignore_parents: Optional[list[int]] = None
|
||||||
|
) -> None:
|
||||||
|
''' Undo target substitution. '''
|
||||||
|
if ignore_parents is None:
|
||||||
|
ignore_parents = []
|
||||||
|
operation_id = target.operation_id
|
||||||
|
original_schema = self.cache.get_schema_by_id(target.original.schema_id)
|
||||||
|
dependant = []
|
||||||
|
for cst_id in original_schema.get_dependant([target.original_id]):
|
||||||
|
if cst_id not in ignore_parents:
|
||||||
|
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
||||||
|
if inheritor_id is not None:
|
||||||
|
dependant.append(inheritor_id)
|
||||||
|
|
||||||
|
self.cache.substitutions[operation_id].remove(target)
|
||||||
|
target.delete()
|
||||||
|
|
||||||
|
new_original: Optional[Constituenta] = None
|
||||||
|
if target.original_id not in ignore_parents:
|
||||||
|
full_cst = Constituenta.objects.get(pk=target.original_id)
|
||||||
|
cst_mapping = create_dependant_mapping(original_schema, [full_cst])
|
||||||
|
self.inherit_cst(operation_id, original_schema, [full_cst], cst_mapping)
|
||||||
|
new_original_id = self.cache.get_inheritor(target.original_id, operation_id)
|
||||||
|
assert new_original_id is not None
|
||||||
|
new_original = schema.cache.by_id[new_original_id]
|
||||||
|
|
||||||
|
if dependant:
|
||||||
|
substitution_id = self.cache.get_inheritor(target.substitution_id, operation_id)
|
||||||
|
assert substitution_id is not None
|
||||||
|
substitution_inheritor = schema.cache.by_id[substitution_id]
|
||||||
|
mapping = {substitution_inheritor.alias: new_original}
|
||||||
|
self._on_partial_mapping(mapping, dependant, operation_id, schema)
|
||||||
|
|
||||||
|
def _determine_insert_position(
|
||||||
|
self, prototype_id: int,
|
||||||
|
operation: Operation,
|
||||||
|
source: RSFormCached,
|
||||||
|
destination: RSFormCached
|
||||||
|
) -> int:
|
||||||
|
''' Determine insert_after for new constituenta. '''
|
||||||
|
prototype = source.cache.by_id[prototype_id]
|
||||||
|
prototype_index = source.cache.constituents.index(prototype)
|
||||||
|
if prototype_index == 0:
|
||||||
|
return 0
|
||||||
|
prev_cst = source.cache.constituents[prototype_index - 1]
|
||||||
|
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
||||||
|
if inherited_prev_id is None:
|
||||||
|
return INSERT_LAST
|
||||||
|
prev_cst = destination.cache.by_id[inherited_prev_id]
|
||||||
|
prev_index = destination.cache.constituents.index(prev_cst)
|
||||||
|
return prev_index + 1
|
||||||
|
|
||||||
|
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping:
|
||||||
|
if not mapping:
|
||||||
|
return mapping
|
||||||
|
result: CstMapping = {}
|
||||||
|
for alias, cst in mapping.items():
|
||||||
|
if cst is None:
|
||||||
|
result[alias] = None
|
||||||
|
continue
|
||||||
|
successor_id = self.cache.get_successor(cst.pk, operation.pk)
|
||||||
|
if successor_id is None:
|
||||||
|
continue
|
||||||
|
successor = schema.cache.by_id.get(successor_id)
|
||||||
|
if successor is None:
|
||||||
|
continue
|
||||||
|
result[alias] = successor
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _transform_substitutions(
|
||||||
|
self,
|
||||||
|
target: CstSubstitution,
|
||||||
|
operation: int,
|
||||||
|
schema: RSFormCached
|
||||||
|
) -> CstSubstitution:
|
||||||
|
result: CstSubstitution = []
|
||||||
|
for current_sub in target:
|
||||||
|
sub_replaced = False
|
||||||
|
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation)
|
||||||
|
if new_substitution_id is None:
|
||||||
|
for sub in self.cache.substitutions[operation]:
|
||||||
|
if sub.original_id == current_sub[1].pk:
|
||||||
|
sub_replaced = True
|
||||||
|
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation)
|
||||||
|
break
|
||||||
|
|
||||||
|
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation)
|
||||||
|
original_replaced = False
|
||||||
|
if new_original_id is None:
|
||||||
|
for sub in self.cache.substitutions[operation]:
|
||||||
|
if sub.original_id == current_sub[0].pk:
|
||||||
|
original_replaced = True
|
||||||
|
sub.original_id = current_sub[1].pk
|
||||||
|
sub.save()
|
||||||
|
new_original_id = new_substitution_id
|
||||||
|
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation)
|
||||||
|
break
|
||||||
|
|
||||||
|
if sub_replaced and original_replaced:
|
||||||
|
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
|
||||||
|
|
||||||
|
for sub in self.cache.substitutions[operation]:
|
||||||
|
if sub.substitution_id == current_sub[0].pk:
|
||||||
|
sub.substitution_id = current_sub[1].pk
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
if new_original_id is not None and new_substitution_id is not None:
|
||||||
|
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _on_partial_mapping(
|
||||||
|
self,
|
||||||
|
mapping: CstMapping,
|
||||||
|
target: list[int],
|
||||||
|
operation: int,
|
||||||
|
schema: RSFormCached
|
||||||
|
) -> None:
|
||||||
|
''' Trigger cascade resolutions when Constituents are partially mapped. '''
|
||||||
|
alias_mapping = cst_mapping_to_alias(mapping)
|
||||||
|
schema.apply_partial_mapping(alias_mapping, target)
|
||||||
|
children = self.cache.extend_graph.outputs[operation]
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded_subs()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is None:
|
||||||
|
continue
|
||||||
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
|
if not new_mapping:
|
||||||
|
continue
|
||||||
|
new_target = self.cache.get_inheritors_list(target, child_id)
|
||||||
|
if not new_target:
|
||||||
|
continue
|
||||||
|
self._on_partial_mapping(new_mapping, new_target, child_id, child_schema)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from apps.library.models import LibraryItem, LibraryItemType
|
from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from apps.rsform.models import Constituenta, CstType, RSFormCached
|
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
|
||||||
|
|
||||||
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ class PropagationFacade:
|
||||||
def before_substitute(sourceID: int, substitutions: CstSubstitution,
|
def before_substitute(sourceID: int, substitutions: CstSubstitution,
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before constituents are substituted. '''
|
''' Trigger cascade resolutions before constituents are substituted. '''
|
||||||
if len(substitutions) == 0:
|
if not substitutions:
|
||||||
return
|
return
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
|
|
@ -73,8 +73,29 @@ class PropagationFacade:
|
||||||
if item.item_type != LibraryItemType.RSFORM:
|
if item.item_type != LibraryItemType.RSFORM:
|
||||||
return
|
return
|
||||||
hosts = _get_oss_hosts(item.pk)
|
hosts = _get_oss_hosts(item.pk)
|
||||||
if len(hosts) == 0:
|
if not hosts:
|
||||||
return
|
return
|
||||||
|
|
||||||
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
|
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
|
||||||
PropagationFacade.before_delete_cst(item.pk, ids, exclude)
|
for host in hosts:
|
||||||
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchemaCached(host).before_delete_cst(item.pk, ids)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def after_create_attribution(sourceID: int, associations: list[Attribution],
|
||||||
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
|
''' Trigger cascade resolutions when Attribution is created. '''
|
||||||
|
hosts = _get_oss_hosts(sourceID)
|
||||||
|
for host in hosts:
|
||||||
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchemaCached(host).after_create_attribution(sourceID, associations)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def before_delete_attribution(sourceID: int,
|
||||||
|
associations: list[Attribution],
|
||||||
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
|
''' Trigger cascade resolutions before Attribution is deleted. '''
|
||||||
|
hosts = _get_oss_hosts(sourceID)
|
||||||
|
for host in hosts:
|
||||||
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchemaCached(host).before_delete_attribution(sourceID, associations)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
''' Models: Operation Reference in OSS. '''
|
|
||||||
from django.db.models import CASCADE, ForeignKey, Model
|
|
||||||
|
|
||||||
|
|
||||||
class Reference(Model):
|
|
||||||
''' Operation Reference. '''
|
|
||||||
reference = ForeignKey(
|
|
||||||
verbose_name='Отсылка',
|
|
||||||
to='oss.Operation',
|
|
||||||
on_delete=CASCADE,
|
|
||||||
related_name='references'
|
|
||||||
)
|
|
||||||
target = ForeignKey(
|
|
||||||
verbose_name='Целевая Операция',
|
|
||||||
to='oss.Operation',
|
|
||||||
on_delete=CASCADE,
|
|
||||||
related_name='targets'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
''' Model metadata. '''
|
|
||||||
verbose_name = 'Отсылка'
|
|
||||||
verbose_name_plural = 'Отсылки'
|
|
||||||
unique_together = [['reference', 'target']]
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f'{self.reference} -> {self.target}'
|
|
||||||
27
rsconcept/backend/apps/oss/models/Replica.py
Normal file
27
rsconcept/backend/apps/oss/models/Replica.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
''' Models: Operation Replica in OSS. '''
|
||||||
|
from django.db.models import CASCADE, ForeignKey, Model
|
||||||
|
|
||||||
|
|
||||||
|
class Replica(Model):
|
||||||
|
''' Operation Replica. '''
|
||||||
|
replica = ForeignKey(
|
||||||
|
verbose_name='Реплика',
|
||||||
|
to='oss.Operation',
|
||||||
|
on_delete=CASCADE,
|
||||||
|
related_name='replicas'
|
||||||
|
)
|
||||||
|
original = ForeignKey(
|
||||||
|
verbose_name='Целевая Операция',
|
||||||
|
to='oss.Operation',
|
||||||
|
on_delete=CASCADE,
|
||||||
|
related_name='targets'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
''' Model metadata. '''
|
||||||
|
verbose_name = 'Реплика'
|
||||||
|
verbose_name_plural = 'Реплики'
|
||||||
|
unique_together = [['replica', 'original']]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.replica} -> {self.original}'
|
||||||
|
|
@ -8,5 +8,5 @@ from .Operation import Operation, OperationType
|
||||||
from .OperationSchema import OperationSchema
|
from .OperationSchema import OperationSchema
|
||||||
from .OperationSchemaCached import OperationSchemaCached
|
from .OperationSchemaCached import OperationSchemaCached
|
||||||
from .PropagationFacade import PropagationFacade
|
from .PropagationFacade import PropagationFacade
|
||||||
from .Reference import Reference
|
from .Replica import Replica
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
|
|
||||||
79
rsconcept/backend/apps/oss/models/utils.py
Normal file
79
rsconcept/backend/apps/oss/models/utils.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
''' Utils for OSS models. '''
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cctext import extract_entities
|
||||||
|
|
||||||
|
from apps.rsform.models import (
|
||||||
|
DELETED_ALIAS,
|
||||||
|
Constituenta,
|
||||||
|
RSFormCached,
|
||||||
|
extract_globals,
|
||||||
|
replace_entities,
|
||||||
|
replace_globals
|
||||||
|
)
|
||||||
|
|
||||||
|
CstMapping = dict[str, Optional[Constituenta]]
|
||||||
|
CstSubstitution = list[tuple[Constituenta, Constituenta]]
|
||||||
|
|
||||||
|
|
||||||
|
def cst_mapping_to_alias(mapping: CstMapping) -> dict[str, str]:
|
||||||
|
''' Convert constituenta mapping to alias mapping. '''
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
for alias, cst in mapping.items():
|
||||||
|
if cst is None:
|
||||||
|
result[alias] = DELETED_ALIAS
|
||||||
|
else:
|
||||||
|
result[alias] = cst.alias
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def map_cst_update_data(cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
|
||||||
|
''' Map data for constituenta update. '''
|
||||||
|
new_data = {}
|
||||||
|
if 'term_forms' in data:
|
||||||
|
if old_data['term_forms'] == cst.term_forms:
|
||||||
|
new_data['term_forms'] = data['term_forms']
|
||||||
|
if 'convention' in data:
|
||||||
|
new_data['convention'] = data['convention']
|
||||||
|
if 'definition_formal' in data:
|
||||||
|
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
|
||||||
|
if 'term_raw' in data:
|
||||||
|
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
|
||||||
|
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
|
||||||
|
if 'definition_raw' in data:
|
||||||
|
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
|
||||||
|
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
|
||||||
|
return new_data
|
||||||
|
|
||||||
|
|
||||||
|
def extract_data_references(data: dict, old_data: dict) -> set[str]:
|
||||||
|
''' Extract references from data. '''
|
||||||
|
result: set[str] = set()
|
||||||
|
if 'definition_formal' in data:
|
||||||
|
result.update(extract_globals(data['definition_formal']))
|
||||||
|
result.update(extract_globals(old_data['definition_formal']))
|
||||||
|
if 'term_raw' in data:
|
||||||
|
result.update(extract_entities(data['term_raw']))
|
||||||
|
result.update(extract_entities(old_data['term_raw']))
|
||||||
|
if 'definition_raw' in data:
|
||||||
|
result.update(extract_entities(data['definition_raw']))
|
||||||
|
result.update(extract_entities(old_data['definition_raw']))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def create_dependant_mapping(source: RSFormCached, cst_list: list[Constituenta]) -> CstMapping:
|
||||||
|
''' Create mapping for dependant Constituents. '''
|
||||||
|
if len(cst_list) == len(source.cache.constituents):
|
||||||
|
return {c.alias: c for c in source.cache.constituents}
|
||||||
|
inserted_aliases = [cst.alias for cst in cst_list]
|
||||||
|
depend_aliases: set[str] = set()
|
||||||
|
for item in cst_list:
|
||||||
|
depend_aliases.update(item.extract_references())
|
||||||
|
depend_aliases.difference_update(inserted_aliases)
|
||||||
|
alias_mapping: CstMapping = {}
|
||||||
|
for alias in depend_aliases:
|
||||||
|
cst = source.cache.by_alias.get(alias)
|
||||||
|
if cst is not None:
|
||||||
|
alias_mapping[alias] = cst
|
||||||
|
return alias_mapping
|
||||||
|
|
@ -6,18 +6,18 @@ from .data_access import (
|
||||||
BlockSerializer,
|
BlockSerializer,
|
||||||
CloneSchemaSerializer,
|
CloneSchemaSerializer,
|
||||||
CreateBlockSerializer,
|
CreateBlockSerializer,
|
||||||
CreateReferenceSerializer,
|
CreateReplicaSerializer,
|
||||||
CreateSchemaSerializer,
|
CreateSchemaSerializer,
|
||||||
CreateSynthesisSerializer,
|
CreateSynthesisSerializer,
|
||||||
DeleteBlockSerializer,
|
DeleteBlockSerializer,
|
||||||
DeleteOperationSerializer,
|
DeleteOperationSerializer,
|
||||||
DeleteReferenceSerializer,
|
DeleteReplicaSerializer,
|
||||||
ImportSchemaSerializer,
|
ImportSchemaSerializer,
|
||||||
MoveItemsSerializer,
|
MoveItemsSerializer,
|
||||||
OperationSchemaSerializer,
|
OperationSchemaSerializer,
|
||||||
OperationSerializer,
|
OperationSerializer,
|
||||||
ReferenceSerializer,
|
|
||||||
RelocateConstituentsSerializer,
|
RelocateConstituentsSerializer,
|
||||||
|
ReplicaSerializer,
|
||||||
SetOperationInputSerializer,
|
SetOperationInputSerializer,
|
||||||
TargetOperationSerializer,
|
TargetOperationSerializer,
|
||||||
UpdateBlockSerializer,
|
UpdateBlockSerializer,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from ..models import (
|
||||||
Layout,
|
Layout,
|
||||||
Operation,
|
Operation,
|
||||||
OperationType,
|
OperationType,
|
||||||
Reference,
|
Replica,
|
||||||
Substitution
|
Substitution
|
||||||
)
|
)
|
||||||
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
|
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
|
||||||
|
|
@ -47,19 +47,19 @@ class BlockSerializer(StrictModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ArgumentSerializer(StrictModelSerializer):
|
class ArgumentSerializer(StrictModelSerializer):
|
||||||
''' Serializer: Operation data. '''
|
''' Serializer: Operation arguments. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Argument
|
model = Argument
|
||||||
fields = ('operation', 'argument')
|
fields = ('operation', 'argument')
|
||||||
|
|
||||||
|
|
||||||
class ReferenceSerializer(StrictModelSerializer):
|
class ReplicaSerializer(StrictModelSerializer):
|
||||||
''' Serializer: Reference data. '''
|
''' Serializer: Replica relation. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Reference
|
model = Replica
|
||||||
fields = ('reference', 'target')
|
fields = ('replica', 'original')
|
||||||
|
|
||||||
|
|
||||||
class CreateBlockSerializer(StrictSerializer):
|
class CreateBlockSerializer(StrictSerializer):
|
||||||
|
|
@ -251,15 +251,15 @@ class CloneSchemaSerializer(StrictSerializer):
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'source_operation': msg.operationResultEmpty(source_operation.alias)
|
'source_operation': msg.operationResultEmpty(source_operation.alias)
|
||||||
})
|
})
|
||||||
if source_operation.operation_type == OperationType.REFERENCE:
|
if source_operation.operation_type == OperationType.REPLICA:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'source_operation': msg.referenceTypeNotAllowed()
|
'source_operation': msg.replicaNotAllowed()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class CreateReferenceSerializer(StrictSerializer):
|
class CreateReplicaSerializer(StrictSerializer):
|
||||||
''' Serializer: Create reference operation. '''
|
''' Serializer: Create Replica operation. '''
|
||||||
layout = serializers.ListField(child=NodeSerializer())
|
layout = serializers.ListField(child=NodeSerializer())
|
||||||
target = PKField(many=False, queryset=Operation.objects.all())
|
target = PKField(many=False, queryset=Operation.objects.all())
|
||||||
position = PositionSerializer()
|
position = PositionSerializer()
|
||||||
|
|
@ -269,11 +269,11 @@ class CreateReferenceSerializer(StrictSerializer):
|
||||||
target = cast(Operation, attrs['target'])
|
target = cast(Operation, attrs['target'])
|
||||||
if target.oss_id != oss.pk:
|
if target.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target_operation': msg.operationNotInOSS()
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
if target.operation_type == OperationType.REFERENCE:
|
if target.operation_type == OperationType.REPLICA:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target_operation': msg.referenceTypeNotAllowed()
|
'target': msg.replicaNotAllowed()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
@ -328,6 +328,10 @@ class CreateSynthesisSerializer(StrictSerializer):
|
||||||
})
|
})
|
||||||
|
|
||||||
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
|
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
|
||||||
|
if len(schemas) != len(set(schemas)):
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'arguments': msg.duplicateSchemasInArguments()
|
||||||
|
})
|
||||||
substitutions = attrs['substitutions']
|
substitutions = attrs['substitutions']
|
||||||
to_delete = {x['original'].pk for x in substitutions}
|
to_delete = {x['original'].pk for x in substitutions}
|
||||||
deleted = set()
|
deleted = set()
|
||||||
|
|
@ -375,6 +379,7 @@ class UpdateOperationSerializer(StrictSerializer):
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
parent = attrs['item_data'].get('parent')
|
parent = attrs['item_data'].get('parent')
|
||||||
|
|
@ -405,6 +410,10 @@ class UpdateOperationSerializer(StrictSerializer):
|
||||||
if 'substitutions' not in attrs:
|
if 'substitutions' not in attrs:
|
||||||
return attrs
|
return attrs
|
||||||
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
|
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
|
||||||
|
if len(schemas) != len(set(schemas)):
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'arguments': msg.duplicateSchemasInArguments()
|
||||||
|
})
|
||||||
substitutions = attrs['substitutions']
|
substitutions = attrs['substitutions']
|
||||||
to_delete = {x['original'].pk for x in substitutions}
|
to_delete = {x['original'].pk for x in substitutions}
|
||||||
deleted = set()
|
deleted = set()
|
||||||
|
|
@ -432,7 +441,7 @@ class UpdateOperationSerializer(StrictSerializer):
|
||||||
|
|
||||||
|
|
||||||
class DeleteOperationSerializer(StrictSerializer):
|
class DeleteOperationSerializer(StrictSerializer):
|
||||||
''' Serializer: Delete non-reference operation. '''
|
''' Serializer: Delete non-replica operation. '''
|
||||||
layout = serializers.ListField(
|
layout = serializers.ListField(
|
||||||
child=NodeSerializer()
|
child=NodeSerializer()
|
||||||
)
|
)
|
||||||
|
|
@ -447,15 +456,15 @@ class DeleteOperationSerializer(StrictSerializer):
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInOSS()
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
if operation.operation_type == OperationType.REFERENCE:
|
if operation.operation_type == OperationType.REPLICA:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.referenceTypeNotAllowed()
|
'target': msg.replicaNotAllowed()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class DeleteReferenceSerializer(StrictSerializer):
|
class DeleteReplicaSerializer(StrictSerializer):
|
||||||
''' Serializer: Delete reference operation. '''
|
''' Serializer: Delete Replica operation. '''
|
||||||
layout = serializers.ListField(
|
layout = serializers.ListField(
|
||||||
child=NodeSerializer()
|
child=NodeSerializer()
|
||||||
)
|
)
|
||||||
|
|
@ -470,9 +479,9 @@ class DeleteReferenceSerializer(StrictSerializer):
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInOSS()
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
if operation.operation_type != OperationType.REFERENCE:
|
if operation.operation_type != OperationType.REPLICA:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.referenceTypeRequired()
|
'target': msg.replicaRequired()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
@ -535,8 +544,8 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
||||||
substitutions = serializers.ListField(
|
substitutions = serializers.ListField(
|
||||||
child=SubstitutionExSerializer()
|
child=SubstitutionExSerializer()
|
||||||
)
|
)
|
||||||
references = serializers.ListField(
|
replicas = serializers.ListField(
|
||||||
child=ReferenceSerializer()
|
child=ReplicaSerializer()
|
||||||
)
|
)
|
||||||
layout = serializers.ListField(
|
layout = serializers.ListField(
|
||||||
child=NodeSerializer()
|
child=NodeSerializer()
|
||||||
|
|
@ -555,7 +564,7 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
||||||
result['blocks'] = []
|
result['blocks'] = []
|
||||||
result['arguments'] = []
|
result['arguments'] = []
|
||||||
result['substitutions'] = []
|
result['substitutions'] = []
|
||||||
result['references'] = []
|
result['replicas'] = []
|
||||||
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
|
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
|
||||||
operation_data = OperationSerializer(operation).data
|
operation_data = OperationSerializer(operation).data
|
||||||
operation_result = operation.result
|
operation_result = operation.result
|
||||||
|
|
@ -578,8 +587,8 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
||||||
substitution_term=F('substitution__term_resolved'),
|
substitution_term=F('substitution__term_resolved'),
|
||||||
).order_by('pk'):
|
).order_by('pk'):
|
||||||
result['substitutions'].append(substitution)
|
result['substitutions'].append(substitution)
|
||||||
for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
|
for replication in Replica.objects.filter(original__oss=instance).order_by('pk'):
|
||||||
result['references'].append(ReferenceSerializer(reference).data)
|
result['replicas'].append(ReplicaSerializer(replication).data)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@ from .t_Argument import *
|
||||||
from .t_Inheritance import *
|
from .t_Inheritance import *
|
||||||
from .t_Layout import *
|
from .t_Layout import *
|
||||||
from .t_Operation import *
|
from .t_Operation import *
|
||||||
from .t_Reference import *
|
from .t_Replica import *
|
||||||
from .t_Substitution import *
|
from .t_Substitution import *
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
''' Testing models: Reference. '''
|
''' Testing models: Replica. '''
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.oss.models import Operation, OperationSchema, OperationType, Reference
|
from apps.oss.models import Operation, OperationSchema, OperationType, Replica
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import RSForm
|
||||||
|
|
||||||
|
|
||||||
class TestReference(TestCase):
|
class TestReplica(TestCase):
|
||||||
''' Testing Reference model. '''
|
''' Testing Replica model. '''
|
||||||
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -19,26 +19,26 @@ class TestReference(TestCase):
|
||||||
)
|
)
|
||||||
self.operation2 = Operation.objects.create(
|
self.operation2 = Operation.objects.create(
|
||||||
oss=self.oss.model,
|
oss=self.oss.model,
|
||||||
operation_type=OperationType.REFERENCE,
|
operation_type=OperationType.REPLICA,
|
||||||
)
|
)
|
||||||
self.reference = Reference.objects.create(
|
self.replicas = Replica.objects.create(
|
||||||
reference=self.operation2,
|
replica=self.operation2,
|
||||||
target=self.operation1
|
original=self.operation1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
testStr = f'{self.operation2} -> {self.operation1}'
|
testStr = f'{self.operation2} -> {self.operation1}'
|
||||||
self.assertEqual(str(self.reference), testStr)
|
self.assertEqual(str(self.replicas), testStr)
|
||||||
|
|
||||||
|
|
||||||
def test_cascade_delete_operation(self):
|
def test_cascade_delete_operation(self):
|
||||||
self.assertEqual(Reference.objects.count(), 1)
|
self.assertEqual(Replica.objects.count(), 1)
|
||||||
self.operation2.delete()
|
self.operation2.delete()
|
||||||
self.assertEqual(Reference.objects.count(), 0)
|
self.assertEqual(Replica.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
def test_cascade_delete_target(self):
|
def test_cascade_delete_target(self):
|
||||||
self.assertEqual(Reference.objects.count(), 1)
|
self.assertEqual(Replica.objects.count(), 1)
|
||||||
self.operation1.delete()
|
self.operation1.delete()
|
||||||
self.assertEqual(Reference.objects.count(), 0)
|
self.assertEqual(Replica.objects.count(), 0)
|
||||||
|
|
@ -73,7 +73,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
def test_set_owner(self):
|
def test_set_owner(self):
|
||||||
data = {'user': self.user3.pk}
|
data = {'user': self.user3.pk}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
|
|
@ -89,7 +89,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
def test_set_location(self):
|
def test_set_location(self):
|
||||||
data = {'location': '/U/temp'}
|
data = {'location': '/U/temp'}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
|
|
@ -105,7 +105,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
def test_set_access_policy(self):
|
def test_set_access_policy(self):
|
||||||
data = {'access_policy': AccessPolicy.PROTECTED}
|
data = {'access_policy': AccessPolicy.PROTECTED}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
|
|
@ -124,7 +124,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
Editor.set(self.ks3.model.pk, [self.user2.pk, self.user.pk])
|
Editor.set(self.ks3.model.pk, [self.user2.pk, self.user.pk])
|
||||||
data = {'users': [self.user3.pk]}
|
data = {'users': [self.user3.pk]}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
|
|
@ -140,7 +140,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
def test_sync_from_result(self):
|
def test_sync_from_result(self):
|
||||||
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
|
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.ks1.model.pk)
|
self.executeOK(data, item=self.ks1.model.pk)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||||
|
|
@ -161,7 +161,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
self.ks3.model.refresh_from_db()
|
self.ks3.model.refresh_from_db()
|
||||||
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
|
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
|
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
'cst_type': CstType.BASE,
|
'cst_type': CstType.BASE,
|
||||||
'definition_formal': 'X4 = X5'
|
'definition_formal': 'X4 = X5'
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, schema=self.ks1.model.pk)
|
response = self.executeCreated(data, schema=self.ks1.model.pk)
|
||||||
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
||||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||||
|
|
@ -125,7 +125,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
'crucial': True,
|
'crucial': True,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response = self.executeOK(data=data, schema=self.ks1.model.pk)
|
response = self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks1X1.refresh_from_db()
|
self.ks1X1.refresh_from_db()
|
||||||
d2.refresh_from_db()
|
d2.refresh_from_db()
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
||||||
|
|
@ -145,7 +145,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
def test_delete_constituenta(self):
|
def test_delete_constituenta(self):
|
||||||
data = {'items': [self.ks2X1.pk]}
|
data = {'items': [self.ks2X1.pk]}
|
||||||
response = self.executeOK(data=data, schema=self.ks2.model.pk)
|
response = self.executeOK(data, schema=self.ks2.model.pk)
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
||||||
self.ks2D1.refresh_from_db()
|
self.ks2D1.refresh_from_db()
|
||||||
self.assertEqual(self.ks2.constituentsQ().count(), 1)
|
self.assertEqual(self.ks2.constituentsQ().count(), 1)
|
||||||
|
|
@ -161,7 +161,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
'original': self.ks1X1.pk,
|
'original': self.ks1X1.pk,
|
||||||
'substitution': self.ks1X2.pk
|
'substitution': self.ks1X2.pk
|
||||||
}]}
|
}]}
|
||||||
self.executeOK(data=data, schema=self.ks1.model.pk)
|
self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks1X2.refresh_from_db()
|
self.ks1X2.refresh_from_db()
|
||||||
d2.refresh_from_db()
|
d2.refresh_from_db()
|
||||||
self.assertEqual(self.ks1.constituentsQ().count(), 1)
|
self.assertEqual(self.ks1.constituentsQ().count(), 1)
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'target': self.operation2.pk
|
'target': self.operation2.pk
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
@ -155,7 +155,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'target': self.operation2.pk,
|
'target': self.operation2.pk,
|
||||||
'input': None
|
'input': None
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
@ -188,7 +188,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'target': self.operation2.pk,
|
'target': self.operation2.pk,
|
||||||
'input': ks6.model.pk
|
'input': ks6.model.pk
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
ks4Dks6 = Constituenta.objects.get(as_child__parent_id=ks6D1.pk)
|
ks4Dks6 = Constituenta.objects.get(as_child__parent_id=ks6D1.pk)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
|
|
@ -234,7 +234,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'delete_schema': True
|
'delete_schema': True
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -256,7 +256,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'delete_schema': True
|
'delete_schema': True
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -278,7 +278,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'delete_schema': False
|
'delete_schema': False
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
@ -317,7 +317,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -343,7 +343,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'arguments': [self.operation1.pk],
|
'arguments': [self.operation1.pk],
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -356,7 +356,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||||
|
|
||||||
data['arguments'] = [self.operation1.pk, self.operation2.pk]
|
data['arguments'] = [self.operation1.pk, self.operation2.pk]
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -381,7 +381,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'target': self.operation4.pk,
|
'target': self.operation4.pk,
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.operation4.refresh_from_db()
|
self.operation4.refresh_from_db()
|
||||||
self.ks5.model.refresh_from_db()
|
self.ks5.model.refresh_from_db()
|
||||||
self.assertNotEqual(self.operation4.result, None)
|
self.assertNotEqual(self.operation4.result, None)
|
||||||
|
|
@ -408,7 +408,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'items': [ks6A1.pk]
|
'items': [ks6A1.pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
ks6.model.refresh_from_db()
|
ks6.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
self.ks4.model.refresh_from_db()
|
self.ks4.model.refresh_from_db()
|
||||||
|
|
@ -438,7 +438,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'items': [self.ks1X2.pk]
|
'items': [self.ks1X2.pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
ks6.model.refresh_from_db()
|
ks6.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
self.ks4.model.refresh_from_db()
|
self.ks4.model.refresh_from_db()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
''' Testing API: Propagate changes through references in OSS. '''
|
''' Testing API: Propagate changes through references in OSS. '''
|
||||||
|
|
||||||
from apps.oss.models import OperationSchema, OperationType
|
from apps.oss.models import Inheritance, OperationSchema, OperationType
|
||||||
from apps.rsform.models import Constituenta, CstType, RSForm
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ class ReferencePropagationTestCase(EndpointTester):
|
||||||
operation_type=OperationType.INPUT,
|
operation_type=OperationType.INPUT,
|
||||||
result=self.ks2.model
|
result=self.ks2.model
|
||||||
)
|
)
|
||||||
self.operation3 = self.owned.create_reference(self.operation1)
|
self.operation3 = self.owned.create_replica(self.operation1)
|
||||||
|
|
||||||
self.operation4 = self.owned.create_operation(
|
self.operation4 = self.owned.create_operation(
|
||||||
alias='4',
|
alias='4',
|
||||||
|
|
@ -79,8 +79,8 @@ class ReferencePropagationTestCase(EndpointTester):
|
||||||
)
|
)
|
||||||
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
|
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
|
||||||
self.owned.set_substitutions(self.operation5.pk, [{
|
self.owned.set_substitutions(self.operation5.pk, [{
|
||||||
'original': self.ks4X1,
|
'original': self.ks1X2,
|
||||||
'substitution': self.ks1X2
|
'substitution': self.ks4X1
|
||||||
}])
|
}])
|
||||||
self.owned.execute_operation(self.operation5)
|
self.owned.execute_operation(self.operation5)
|
||||||
self.operation5.refresh_from_db()
|
self.operation5.refresh_from_db()
|
||||||
|
|
@ -97,8 +97,8 @@ class ReferencePropagationTestCase(EndpointTester):
|
||||||
)
|
)
|
||||||
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
|
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
|
||||||
self.owned.set_substitutions(self.operation6.pk, [{
|
self.owned.set_substitutions(self.operation6.pk, [{
|
||||||
'original': self.ks2X1,
|
'original': self.ks2X2,
|
||||||
'substitution': self.ks1X1
|
'substitution': self.ks1X2
|
||||||
}])
|
}])
|
||||||
self.owned.execute_operation(self.operation6)
|
self.owned.execute_operation(self.operation6)
|
||||||
self.operation6.refresh_from_db()
|
self.operation6.refresh_from_db()
|
||||||
|
|
@ -139,8 +139,68 @@ class ReferencePropagationTestCase(EndpointTester):
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'target': self.operation1.pk
|
'target': self.operation1.pk
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(self.ks6.constituentsQ().count(), 4)
|
self.assertEqual(self.ks6.constituentsQ().count(), 4)
|
||||||
# self.assertEqual(self.ks5.constituentsQ().count(), 5)
|
self.assertEqual(self.ks5.constituentsQ().count(), 5)
|
||||||
|
|
||||||
# TODO: add more tests
|
|
||||||
|
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
|
||||||
|
def test_create_constituenta(self):
|
||||||
|
data = {
|
||||||
|
'alias': 'X3',
|
||||||
|
'cst_type': CstType.BASE,
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data, schema=self.ks1.model.pk)
|
||||||
|
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
||||||
|
inherited = Constituenta.objects.filter(as_child__parent_id=new_cst.pk)
|
||||||
|
self.assertEqual(self.ks1.constituentsQ().count(), 4)
|
||||||
|
self.assertEqual(self.ks4.constituentsQ().count(), 7)
|
||||||
|
self.assertEqual(self.ks5.constituentsQ().count(), 11)
|
||||||
|
self.assertEqual(self.ks6.constituentsQ().count(), 7)
|
||||||
|
self.assertEqual(inherited.count(), 3)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
|
def test_delete_constituenta(self):
|
||||||
|
data = {'items': [self.ks1X1.pk]}
|
||||||
|
response = self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
|
self.ks4D2.refresh_from_db()
|
||||||
|
self.ks5D4.refresh_from_db()
|
||||||
|
self.ks6D2.refresh_from_db()
|
||||||
|
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
|
||||||
|
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 DEL S1 D1 D2 D3')
|
||||||
|
self.assertEqual(self.ks6D2.definition_formal, r'X1 DEL X3 S1 D1')
|
||||||
|
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||||
|
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||||
|
self.assertEqual(self.ks6.constituentsQ().count(), 5)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/delete-replica', method='patch')
|
||||||
|
def test_delete_replica_redirection(self):
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'target': self.operation3.pk,
|
||||||
|
'keep_connections': True,
|
||||||
|
'keep_constituents': False
|
||||||
|
}
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||||
|
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||||
|
self.assertEqual(self.ks6.constituentsQ().count(), 6)
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/delete-replica', method='patch')
|
||||||
|
def test_delete_replica_constituents(self):
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'target': self.operation3.pk,
|
||||||
|
'keep_connections': False,
|
||||||
|
'keep_constituents': True
|
||||||
|
}
|
||||||
|
ks5X4 = Constituenta.objects.get(schema=self.ks5.model, alias='X4')
|
||||||
|
self.assertEqual(Inheritance.objects.filter(child=ks5X4).count(), 1)
|
||||||
|
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||||
|
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||||
|
self.assertEqual(self.ks6.constituentsQ().count(), 7)
|
||||||
|
self.assertEqual(Inheritance.objects.filter(child=ks5X4).count(), 0)
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ class TestChangeSubstitutions(EndpointTester):
|
||||||
'original': self.ks1X1.pk,
|
'original': self.ks1X1.pk,
|
||||||
'substitution': self.ks1X2.pk
|
'substitution': self.ks1X2.pk
|
||||||
}]}
|
}]}
|
||||||
self.executeOK(data=data, schema=self.ks1.model.pk)
|
self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
@ -159,7 +159,7 @@ class TestChangeSubstitutions(EndpointTester):
|
||||||
'substitution': self.ks2X1.pk
|
'substitution': self.ks2X1.pk
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, schema=self.ks2.model.pk)
|
self.executeOK(data, schema=self.ks2.model.pk)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
@ -179,7 +179,7 @@ class TestChangeSubstitutions(EndpointTester):
|
||||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
def test_delete_original(self):
|
def test_delete_original(self):
|
||||||
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
|
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
|
||||||
self.executeOK(data=data, schema=self.ks1.model.pk)
|
self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
subs1_2 = self.operation4.getQ_substitutions()
|
subs1_2 = self.operation4.getQ_substitutions()
|
||||||
|
|
@ -194,7 +194,7 @@ class TestChangeSubstitutions(EndpointTester):
|
||||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
def test_delete_substitution(self):
|
def test_delete_substitution(self):
|
||||||
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}
|
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}
|
||||||
self.executeOK(data=data, schema=self.ks2.model.pk)
|
self.executeOK(data, schema=self.ks2.model.pk)
|
||||||
self.ks4D1.refresh_from_db()
|
self.ks4D1.refresh_from_db()
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,9 @@ class TestOssBlocks(EndpointTester):
|
||||||
'children_operations': [],
|
'children_operations': [],
|
||||||
'children_blocks': []
|
'children_blocks': []
|
||||||
}
|
}
|
||||||
self.executeNotFound(data=data, item=self.invalid_id)
|
self.executeNotFound(data, item=self.invalid_id)
|
||||||
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.assertEqual(len(response.data['oss']['blocks']), 3)
|
self.assertEqual(len(response.data['oss']['blocks']), 3)
|
||||||
new_block = response.data['new_block']
|
new_block = response.data['new_block']
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
|
|
@ -94,9 +94,9 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.assertEqual(block_node['height'], data['position']['height'])
|
self.assertEqual(block_node['height'], data['position']['height'])
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeCreated(data=data, item=self.unowned_id)
|
self.executeCreated(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/create-block', method='post')
|
@decl_endpoint('/api/oss/{item}/create-block', method='post')
|
||||||
|
|
@ -118,13 +118,13 @@ class TestOssBlocks(EndpointTester):
|
||||||
'children_operations': [],
|
'children_operations': [],
|
||||||
'children_blocks': []
|
'children_blocks': []
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['item_data']['parent'] = self.block3.pk
|
data['item_data']['parent'] = self.block3.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['item_data']['parent'] = self.block1.pk
|
data['item_data']['parent'] = self.block1.pk
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
new_block = response.data['new_block']
|
new_block = response.data['new_block']
|
||||||
block_data = next((block for block in response.data['oss']['blocks'] if block['id'] == new_block), None)
|
block_data = next((block for block in response.data['oss']['blocks'] if block['id'] == new_block), None)
|
||||||
self.assertEqual(block_data['parent'], self.block1.pk)
|
self.assertEqual(block_data['parent'], self.block1.pk)
|
||||||
|
|
@ -148,20 +148,20 @@ class TestOssBlocks(EndpointTester):
|
||||||
'children_operations': [self.invalid_id],
|
'children_operations': [self.invalid_id],
|
||||||
'children_blocks': []
|
'children_blocks': []
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['children_operations'] = [self.operation3.pk]
|
data['children_operations'] = [self.operation3.pk]
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['children_operations'] = [self.block1.pk]
|
data['children_operations'] = [self.block1.pk]
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['children_operations'] = [self.operation1.pk]
|
data['children_operations'] = [self.operation1.pk]
|
||||||
data['children_blocks'] = [self.operation1.pk]
|
data['children_blocks'] = [self.operation1.pk]
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['children_blocks'] = [self.block1.pk]
|
data['children_blocks'] = [self.block1.pk]
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
new_block = response.data['new_block']
|
new_block = response.data['new_block']
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.block1.refresh_from_db()
|
self.block1.refresh_from_db()
|
||||||
|
|
@ -188,13 +188,13 @@ class TestOssBlocks(EndpointTester):
|
||||||
'children_operations': [],
|
'children_operations': [],
|
||||||
'children_blocks': [self.block1.pk]
|
'children_blocks': [self.block1.pk]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['item_data']['parent'] = self.block1.pk
|
data['item_data']['parent'] = self.block1.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['children_blocks'] = [self.block2.pk]
|
data['children_blocks'] = [self.block2.pk]
|
||||||
self.executeCreated(data=data)
|
self.executeCreated(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
|
||||||
|
|
@ -206,26 +206,26 @@ class TestOssBlocks(EndpointTester):
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.block3.pk
|
data['target'] = self.block3.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.block2.pk
|
data['target'] = self.block2.pk
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data)
|
self.executeForbidden(data)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation2.refresh_from_db()
|
self.operation2.refresh_from_db()
|
||||||
self.assertEqual(len(response.data['blocks']), 1)
|
self.assertEqual(len(response.data['blocks']), 1)
|
||||||
self.assertEqual(self.operation2.parent.pk, self.block1.pk)
|
self.assertEqual(self.operation2.parent.pk, self.block1.pk)
|
||||||
|
|
||||||
data['target'] = self.block1.pk
|
data['target'] = self.block1.pk
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.operation2.refresh_from_db()
|
self.operation2.refresh_from_db()
|
||||||
self.assertEqual(len(response.data['blocks']), 0)
|
self.assertEqual(len(response.data['blocks']), 0)
|
||||||
|
|
@ -246,25 +246,25 @@ class TestOssBlocks(EndpointTester):
|
||||||
'parent': None
|
'parent': None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.block3.pk
|
data['target'] = self.block3.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.block2.pk
|
data['target'] = self.block2.pk
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data)
|
self.executeForbidden(data)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.block2.refresh_from_db()
|
self.block2.refresh_from_db()
|
||||||
self.assertEqual(self.block2.title, data['item_data']['title'])
|
self.assertEqual(self.block2.title, data['item_data']['title'])
|
||||||
self.assertEqual(self.block2.description, data['item_data']['description'])
|
self.assertEqual(self.block2.description, data['item_data']['description'])
|
||||||
self.assertEqual(self.block2.parent, data['item_data']['parent'])
|
self.assertEqual(self.block2.parent, data['item_data']['parent'])
|
||||||
|
|
||||||
data['layout'] = self.layout_data
|
data['layout'] = self.layout_data
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-block', method='patch')
|
@decl_endpoint('/api/oss/{item}/update-block', method='patch')
|
||||||
|
|
@ -280,13 +280,13 @@ class TestOssBlocks(EndpointTester):
|
||||||
'parent': self.block2.pk
|
'parent': self.block2.pk
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
# Create a deeper hierarchy: block1 -> block2 -> block3
|
# Create a deeper hierarchy: block1 -> block2 -> block3
|
||||||
self.block3 = self.owned.create_block(title='3', parent=self.block2)
|
self.block3 = self.owned.create_block(title='3', parent=self.block2)
|
||||||
# Try to set block1's parent to block3 (should fail, indirect cycle)
|
# Try to set block1's parent to block3 (should fail, indirect cycle)
|
||||||
data['item_data']['parent'] = self.block3.pk
|
data['item_data']['parent'] = self.block3.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
# Setting block2's parent to block1 (valid, as block1 is not a descendant)
|
# Setting block2's parent to block1 (valid, as block1 is not a descendant)
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -297,4 +297,4 @@ class TestOssBlocks(EndpointTester):
|
||||||
'parent': self.block1.pk
|
'parent': self.block1.pk
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
''' Testing API: Operation Schema - operations manipulation. '''
|
''' Testing API: Operation Schema - operations manipulation. '''
|
||||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||||
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Reference
|
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Replica
|
||||||
from apps.rsform.models import Constituenta, RSForm
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
@ -95,9 +95,9 @@ class TestOssOperations(EndpointTester):
|
||||||
'height': 50
|
'height': 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.executeNotFound(data=data, item=self.invalid_id)
|
self.executeNotFound(data, item=self.invalid_id)
|
||||||
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.assertEqual(len(response.data['oss']['operations']), 4)
|
self.assertEqual(len(response.data['oss']['operations']), 4)
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
|
|
@ -122,9 +122,9 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(schema.location, self.owned.model.location)
|
self.assertEqual(schema.location, self.owned.model.location)
|
||||||
self.assertIn(self.user2, schema.getQ_editors())
|
self.assertIn(self.user2, schema.getQ_editors())
|
||||||
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeCreated(data=data, item=self.unowned_id)
|
self.executeCreated(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/clone-schema', method='post')
|
@decl_endpoint('/api/oss/{item}/clone-schema', method='post')
|
||||||
|
|
@ -141,10 +141,10 @@ class TestOssOperations(EndpointTester):
|
||||||
'height': 60
|
'height': 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.executeNotFound(data=data, item=self.invalid_id)
|
self.executeNotFound(data, item=self.invalid_id)
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.assertIn('new_operation', response.data)
|
self.assertIn('new_operation', response.data)
|
||||||
self.assertIn('oss', response.data)
|
self.assertIn('oss', response.data)
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
|
|
@ -171,7 +171,7 @@ class TestOssOperations(EndpointTester):
|
||||||
|
|
||||||
unrelated_data = dict(data)
|
unrelated_data = dict(data)
|
||||||
unrelated_data['source_operation'] = self.unowned_operation.pk
|
unrelated_data['source_operation'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=unrelated_data, item=self.owned_id)
|
self.executeBadData(unrelated_data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/create-schema', method='post')
|
@decl_endpoint('/api/oss/{item}/create-schema', method='post')
|
||||||
|
|
@ -194,23 +194,23 @@ class TestOssOperations(EndpointTester):
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
block_unowned = self.unowned.create_block(title='TestBlock1')
|
block_unowned = self.unowned.create_block(title='TestBlock1')
|
||||||
data['item_data']['parent'] = block_unowned.id
|
data['item_data']['parent'] = block_unowned.id
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
block_owned = self.owned.create_block(title='TestBlock2')
|
block_owned = self.owned.create_block(title='TestBlock2')
|
||||||
data['item_data']['parent'] = block_owned.id
|
data['item_data']['parent'] = block_owned.id
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
self.assertEqual(len(response.data['oss']['operations']), 4)
|
self.assertEqual(len(response.data['oss']['operations']), 4)
|
||||||
self.assertEqual(new_operation['parent'], block_owned.id)
|
self.assertEqual(new_operation['parent'], block_owned.id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/create-reference', method='post')
|
@decl_endpoint('/api/oss/{item}/create-replica', method='post')
|
||||||
def test_create_reference(self):
|
def test_create_replica(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
data = {
|
data = {
|
||||||
'target': self.invalid_id,
|
'target': self.invalid_id,
|
||||||
|
|
@ -222,20 +222,20 @@ class TestOssOperations(EndpointTester):
|
||||||
'height': 40
|
'height': 40
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = self.unowned_operation.pk
|
data['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
self.assertEqual(new_operation['operation_type'], OperationType.REFERENCE)
|
self.assertEqual(new_operation['operation_type'], OperationType.REPLICA)
|
||||||
self.assertEqual(new_operation['parent'], self.operation1.parent_id)
|
self.assertEqual(new_operation['parent'], self.operation1.parent_id)
|
||||||
self.assertEqual(new_operation['result'], self.operation1.result_id)
|
self.assertEqual(new_operation['result'], self.operation1.result_id)
|
||||||
ref = Reference.objects.filter(reference_id=new_operation_id, target_id=self.operation1.pk).first()
|
ref = Replica.objects.filter(replica_id=new_operation_id, original_id=self.operation1.pk).first()
|
||||||
self.assertIsNotNone(ref)
|
self.assertIsNotNone(ref)
|
||||||
self.assertTrue(Operation.objects.filter(pk=new_operation_id, oss=self.owned.model).exists())
|
self.assertTrue(Operation.objects.filter(pk=new_operation_id, oss=self.owned.model).exists())
|
||||||
|
|
||||||
|
|
@ -260,7 +260,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'arguments': [self.operation1.pk, self.operation3.pk],
|
'arguments': [self.operation1.pk, self.operation3.pk],
|
||||||
'substitutions': []
|
'substitutions': []
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
|
|
@ -270,6 +270,37 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertNotEqual(new_operation['result'], None)
|
self.assertNotEqual(new_operation['result'], None)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/create-synthesis', method='post')
|
||||||
|
def test_create_synthesis_replicas(self):
|
||||||
|
self.populateData()
|
||||||
|
operation4 = self.owned.create_replica(self.operation1)
|
||||||
|
operation5 = self.owned.create_replica(self.operation1)
|
||||||
|
data = {
|
||||||
|
'item_data': {
|
||||||
|
'alias': 'Test5',
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': '',
|
||||||
|
'parent': None
|
||||||
|
},
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'position': {
|
||||||
|
'x': 1,
|
||||||
|
'y': 1,
|
||||||
|
'width': 500,
|
||||||
|
'height': 50
|
||||||
|
},
|
||||||
|
'arguments': [self.operation1.pk, operation4.pk],
|
||||||
|
'substitutions': []
|
||||||
|
}
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['arguments'] = [operation4.pk, operation5.pk]
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['arguments'] = [operation4.pk, self.operation3.pk]
|
||||||
|
self.executeCreated(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||||
def test_delete_operation(self):
|
def test_delete_operation(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
@ -279,19 +310,19 @@ class TestOssOperations(EndpointTester):
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.unowned_operation.pk
|
data['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
layout = response.data['layout']
|
layout = response.data['layout']
|
||||||
deleted_items = [item for item in layout if item['nodeID'] == 'o' + str(data['target'])]
|
deleted_items = [item for item in layout if item['nodeID'] == 'o' + str(data['target'])]
|
||||||
self.assertEqual(len(response.data['operations']), 2)
|
self.assertEqual(len(response.data['operations']), 2)
|
||||||
|
|
@ -301,34 +332,34 @@ class TestOssOperations(EndpointTester):
|
||||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||||
def test_delete_reference_operation_invalid(self):
|
def test_delete_reference_operation_invalid(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
reference_operation = self.owned.create_reference(self.operation1)
|
reference_operation = self.owned.create_replica(self.operation1)
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'target': reference_operation.pk
|
'target': reference_operation.pk
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/delete-reference', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-replica', method='patch')
|
||||||
def test_delete_reference_operation(self):
|
def test_delete_replica_operation(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'target': self.invalid_id
|
'target': self.invalid_id
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
reference_operation = self.owned.create_reference(self.operation1)
|
reference_operation = self.owned.create_replica(self.operation1)
|
||||||
self.assertEqual(len(self.operation1.getQ_references()), 1)
|
self.assertEqual(len(self.operation1.getQ_replicas()), 1)
|
||||||
data['target'] = reference_operation.pk
|
data['target'] = reference_operation.pk
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = reference_operation.pk
|
data['target'] = reference_operation.pk
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(len(self.operation1.getQ_references()), 0)
|
self.assertEqual(len(self.operation1.getQ_replicas()), 0)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
|
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
|
||||||
|
|
@ -339,22 +370,22 @@ class TestOssOperations(EndpointTester):
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
self.operation1.result = None
|
self.operation1.result = None
|
||||||
self.operation1.description = 'TestComment'
|
self.operation1.description = 'TestComment'
|
||||||
self.operation1.title = 'TestTitle'
|
self.operation1.title = 'TestTitle'
|
||||||
self.operation1.save()
|
self.operation1.save()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
|
|
||||||
new_schema = response.data['new_schema']
|
new_schema = response.data['new_schema']
|
||||||
|
|
@ -364,10 +395,10 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(new_schema['description'], self.operation1.description)
|
self.assertEqual(new_schema['description'], self.operation1.description)
|
||||||
|
|
||||||
data['target'] = self.operation3.pk
|
data['target'] = self.operation3.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.unowned_operation.pk
|
data['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||||
|
|
@ -378,17 +409,17 @@ class TestOssOperations(EndpointTester):
|
||||||
data = {
|
data = {
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
data['input'] = None
|
data['input'] = None
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.result, None)
|
self.assertEqual(self.operation1.result, None)
|
||||||
|
|
||||||
|
|
@ -397,7 +428,7 @@ class TestOssOperations(EndpointTester):
|
||||||
self.ks1.model.title = 'Test421'
|
self.ks1.model.title = 'Test421'
|
||||||
self.ks1.model.description = 'TestComment42'
|
self.ks1.model.description = 'TestComment42'
|
||||||
self.ks1.model.save()
|
self.ks1.model.save()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||||
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
|
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
|
||||||
|
|
@ -415,7 +446,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'target': self.operation1.pk,
|
'target': self.operation1.pk,
|
||||||
'input': self.ks2.model.pk
|
'input': self.ks2.model.pk
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
self.ks2.model.visible = False
|
self.ks2.model.visible = False
|
||||||
self.ks2.model.save(update_fields=['visible'])
|
self.ks2.model.save(update_fields=['visible'])
|
||||||
|
|
@ -424,7 +455,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'target': self.operation2.pk,
|
'target': self.operation2.pk,
|
||||||
'input': None
|
'input': None
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.operation2.refresh_from_db()
|
self.operation2.refresh_from_db()
|
||||||
self.ks2.model.refresh_from_db()
|
self.ks2.model.refresh_from_db()
|
||||||
self.assertEqual(self.operation2.result, None)
|
self.assertEqual(self.operation2.result, None)
|
||||||
|
|
@ -435,7 +466,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'target': self.operation1.pk,
|
'target': self.operation1.pk,
|
||||||
'input': self.ks2.model.pk
|
'input': self.ks2.model.pk
|
||||||
}
|
}
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.result, self.ks2.model)
|
self.assertEqual(self.operation1.result, self.ks2.model)
|
||||||
|
|
||||||
|
|
@ -463,16 +494,16 @@ class TestOssOperations(EndpointTester):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['substitutions'][0]['substitution'] = self.ks2X1.pk
|
data['substitutions'][0]['substitution'] = self.ks2X1.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation3.refresh_from_db()
|
self.operation3.refresh_from_db()
|
||||||
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'])
|
||||||
|
|
@ -487,11 +518,11 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
||||||
|
|
||||||
data['layout'] = self.layout_data
|
data['layout'] = self.layout_data
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
|
|
||||||
data_bad = dict(data)
|
data_bad = dict(data)
|
||||||
data_bad['target'] = self.unowned_operation.pk
|
data_bad['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data_bad, item=self.owned_id)
|
self.executeBadData(data_bad, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
|
|
@ -508,10 +539,10 @@ class TestOssOperations(EndpointTester):
|
||||||
},
|
},
|
||||||
'layout': self.layout_data
|
'layout': self.layout_data
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = self.operation1.pk
|
data['target'] = self.operation1.pk
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(self.operation1.title, data['item_data']['title'])
|
self.assertEqual(self.operation1.title, data['item_data']['title'])
|
||||||
|
|
@ -523,7 +554,7 @@ class TestOssOperations(EndpointTester):
|
||||||
# Try to update an operation from an unrelated OSS (should fail)
|
# Try to update an operation from an unrelated OSS (should fail)
|
||||||
data_bad = dict(data)
|
data_bad = dict(data)
|
||||||
data_bad['target'] = self.unowned_operation.pk
|
data_bad['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data_bad, item=self.owned_id)
|
self.executeBadData(data_bad, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
|
|
@ -552,7 +583,7 @@ class TestOssOperations(EndpointTester):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
|
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
|
||||||
|
|
@ -564,19 +595,19 @@ class TestOssOperations(EndpointTester):
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'target': self.operation1.pk
|
'target': self.operation1.pk
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['target'] = self.unowned_operation.pk
|
data['target'] = self.unowned_operation.pk
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data['target'] = self.operation3.pk
|
data['target'] = self.operation3.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.operation3.refresh_from_db()
|
self.operation3.refresh_from_db()
|
||||||
schema = self.operation3.result
|
schema = self.operation3.result
|
||||||
self.assertEqual(schema.alias, self.operation3.alias)
|
self.assertEqual(schema.alias, self.operation3.alias)
|
||||||
|
|
@ -613,7 +644,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'source': target_ks.model.pk,
|
'source': target_ks.model.pk,
|
||||||
'clone_source': False
|
'clone_source': False
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
|
|
@ -653,7 +684,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'source': self.ks2.model.pk,
|
'source': self.ks2.model.pk,
|
||||||
'clone_source': True
|
'clone_source': True
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
new_operation_id = response.data['new_operation']
|
new_operation_id = response.data['new_operation']
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
|
|
@ -694,13 +725,13 @@ class TestOssOperations(EndpointTester):
|
||||||
# 'source' missing
|
# 'source' missing
|
||||||
'clone_source': False
|
'clone_source': False
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
# Invalid source
|
# Invalid source
|
||||||
data['source'] = self.invalid_id
|
data['source'] = self.invalid_id
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
# Invalid OSS
|
# Invalid OSS
|
||||||
data['source'] = self.ks1.model.pk
|
data['source'] = self.ks1.model.pk
|
||||||
self.executeNotFound(data=data, item=self.invalid_id)
|
self.executeNotFound(data, item=self.invalid_id)
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
||||||
def test_import_schema_permissions(self):
|
def test_import_schema_permissions(self):
|
||||||
|
|
@ -721,8 +752,8 @@ class TestOssOperations(EndpointTester):
|
||||||
}
|
}
|
||||||
# Not an editor
|
# Not an editor
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden(data=data, item=self.owned_id)
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
# As admin
|
# As admin
|
||||||
self.login()
|
self.login()
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeCreated(data=data, item=self.owned_id)
|
self.executeCreated(data, item=self.owned_id)
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.executeBadData(item=self.owned_id)
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
data = {'data': []}
|
data = {'data': []}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
|
|
||||||
data = {'data': [
|
data = {'data': [
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 42.1, 'y': 1337, 'width': 150, 'height': 40},
|
{'nodeID': 'o' + str(self.operation1.pk), 'x': 42.1, 'y': 1337, 'width': 150, 'height': 40},
|
||||||
|
|
@ -134,15 +134,15 @@ class TestOssViewset(EndpointTester):
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 36.1, 'y': 1435, 'width': 150, 'height': 40}
|
{'nodeID': 'o' + str(self.operation3.pk), 'x': 36.1, 'y': 1435, 'width': 150, 'height': 40}
|
||||||
]}
|
]}
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeOK(data=data, item=self.unowned_id)
|
self.executeOK(data, item=self.unowned_id)
|
||||||
|
|
||||||
self.toggle_admin(False)
|
self.toggle_admin(False)
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data, item=self.owned_id)
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data'])
|
self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data'])
|
||||||
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
self.executeForbidden(data=data, item=self.private_id)
|
self.executeForbidden(data, item=self.private_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/get-predecessor', method='post')
|
@decl_endpoint('/api/oss/get-predecessor', method='post')
|
||||||
|
|
@ -155,13 +155,13 @@ class TestOssViewset(EndpointTester):
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||||
|
|
||||||
self.executeBadData(data={'target': self.invalid_id})
|
self.executeBadData({'target': self.invalid_id})
|
||||||
|
|
||||||
response = self.executeOK(data={'target': self.ks1X1.pk})
|
response = self.executeOK({'target': self.ks1X1.pk})
|
||||||
self.assertEqual(response.data['id'], self.ks1X1.pk)
|
self.assertEqual(response.data['id'], self.ks1X1.pk)
|
||||||
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
||||||
|
|
||||||
response = self.executeOK(data={'target': self.ks3X2.pk})
|
response = self.executeOK({'target': self.ks3X2.pk})
|
||||||
self.assertEqual(response.data['id'], self.ks1X2.pk)
|
self.assertEqual(response.data['id'], self.ks1X2.pk)
|
||||||
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
||||||
|
|
||||||
|
|
@ -180,10 +180,10 @@ class TestOssViewset(EndpointTester):
|
||||||
'operations': [self.operation1.pk, self.operation2.pk],
|
'operations': [self.operation1.pk, self.operation2.pk],
|
||||||
'destination': block2.pk
|
'destination': block2.pk
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['destination'] = block1.pk
|
data['destination'] = block1.pk
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.operation2.refresh_from_db()
|
self.operation2.refresh_from_db()
|
||||||
block2.refresh_from_db()
|
block2.refresh_from_db()
|
||||||
|
|
@ -193,7 +193,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(block2.parent.pk, block1.pk)
|
self.assertEqual(block2.parent.pk, block1.pk)
|
||||||
|
|
||||||
data['destination'] = None
|
data['destination'] = None
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.operation2.refresh_from_db()
|
self.operation2.refresh_from_db()
|
||||||
block2.refresh_from_db()
|
block2.refresh_from_db()
|
||||||
|
|
@ -217,7 +217,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'operations': [],
|
'operations': [],
|
||||||
'destination': block3.pk
|
'destination': block3.pk
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
|
|
@ -236,35 +236,35 @@ class TestOssViewset(EndpointTester):
|
||||||
'destination': self.invalid_id,
|
'destination': self.invalid_id,
|
||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
# empty items
|
# empty items
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks1.model.pk,
|
'destination': self.ks1.model.pk,
|
||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
# source == destination
|
# source == destination
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks1.model.pk,
|
'destination': self.ks1.model.pk,
|
||||||
'items': [self.ks1X1.pk]
|
'items': [self.ks1X1.pk]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
# moving inherited
|
# moving inherited
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks1.model.pk,
|
'destination': self.ks1.model.pk,
|
||||||
'items': [self.ks3X2.pk]
|
'items': [self.ks3X2.pk]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
# source and destination are not connected
|
# source and destination are not connected
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks2.model.pk,
|
'destination': self.ks2.model.pk,
|
||||||
'items': [self.ks1X1.pk]
|
'items': [self.ks1X1.pk]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks3.model.pk,
|
'destination': self.ks3.model.pk,
|
||||||
|
|
@ -272,14 +272,14 @@ class TestOssViewset(EndpointTester):
|
||||||
}
|
}
|
||||||
self.ks3X2.refresh_from_db()
|
self.ks3X2.refresh_from_db()
|
||||||
self.assertEqual(self.ks3X2.convention, 'test')
|
self.assertEqual(self.ks3X2.convention, 'test')
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1X2.pk).exists())
|
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1X2.pk).exists())
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'destination': self.ks1.model.pk,
|
'destination': self.ks1.model.pk,
|
||||||
'items': [self.ks3X10.pk]
|
'items': [self.ks3X10.pk]
|
||||||
}
|
}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
self.assertTrue(Constituenta.objects.filter(as_parent__child_id=self.ks3X10.pk).exists())
|
self.assertTrue(Constituenta.objects.filter(as_parent__child_id=self.ks3X10.pk).exists())
|
||||||
self.ks1X3 = Constituenta.objects.get(as_parent__child_id=self.ks3X10.pk)
|
self.ks1X3 = Constituenta.objects.get(as_parent__child_id=self.ks3X10.pk)
|
||||||
self.assertEqual(self.ks1X3.convention, 'test2')
|
self.assertEqual(self.ks1X3.convention, 'test2')
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'create_schema',
|
'create_schema',
|
||||||
'clone_schema',
|
'clone_schema',
|
||||||
'import_schema',
|
'import_schema',
|
||||||
'create_reference',
|
'create_replica',
|
||||||
'create_synthesis',
|
'create_synthesis',
|
||||||
'update_operation',
|
'update_operation',
|
||||||
'delete_operation',
|
'delete_operation',
|
||||||
'delete_reference',
|
'delete_replica',
|
||||||
'create_input',
|
'create_input',
|
||||||
'set_input',
|
'set_input',
|
||||||
'execute_operation',
|
'execute_operation',
|
||||||
|
|
@ -140,10 +140,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def create_block(self, request: Request, pk) -> HttpResponse:
|
def create_block(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create Block. '''
|
''' Create Block. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CreateBlockSerializer(
|
serializer = s.CreateBlockSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -161,11 +158,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'height': position['height'],
|
'height': position['height'],
|
||||||
})
|
})
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
if len(children_blocks) > 0:
|
if children_blocks:
|
||||||
for block in children_blocks:
|
for block in children_blocks:
|
||||||
block.parent = new_block
|
block.parent = new_block
|
||||||
m.Block.objects.bulk_update(children_blocks, ['parent'])
|
m.Block.objects.bulk_update(children_blocks, ['parent'])
|
||||||
if len(children_operations) > 0:
|
if children_operations:
|
||||||
for operation in children_operations:
|
for operation in children_operations:
|
||||||
operation.parent = new_block
|
operation.parent = new_block
|
||||||
m.Operation.objects.bulk_update(children_operations, ['parent'])
|
m.Operation.objects.bulk_update(children_operations, ['parent'])
|
||||||
|
|
@ -194,10 +191,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def update_block(self, request: Request, pk) -> HttpResponse:
|
def update_block(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Update Block. '''
|
''' Update Block. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.UpdateBlockSerializer(
|
serializer = s.UpdateBlockSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
block: m.Block = cast(m.Block, serializer.validated_data['target'])
|
block: m.Block = cast(m.Block, serializer.validated_data['target'])
|
||||||
|
|
||||||
|
|
@ -234,10 +228,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def delete_block(self, request: Request, pk) -> HttpResponse:
|
def delete_block(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete Block. '''
|
''' Endpoint: Delete Block. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.DeleteBlockSerializer(
|
serializer = s.DeleteBlockSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
block = cast(m.Block, serializer.validated_data['target'])
|
block = cast(m.Block, serializer.validated_data['target'])
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
|
|
@ -269,10 +260,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def move_items(self, request: Request, pk) -> HttpResponse:
|
def move_items(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Move items to another parent. '''
|
''' Move items to another parent. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.MoveItemsSerializer(
|
serializer = s.MoveItemsSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
|
|
||||||
|
|
@ -306,10 +294,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def create_schema(self, request: Request, pk) -> HttpResponse:
|
def create_schema(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create schema. '''
|
''' Create schema. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CreateSchemaSerializer(
|
serializer = s.CreateSchemaSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -327,7 +312,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'height': position['height']
|
'height': position['height']
|
||||||
})
|
})
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
oss.create_input(new_operation)
|
m.OperationSchema.create_input(item, new_operation)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -354,10 +339,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def clone_schema(self, request: Request, pk) -> HttpResponse:
|
def clone_schema(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Clone schema. '''
|
''' Clone schema. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CloneSchemaSerializer(
|
serializer = s.CloneSchemaSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -424,10 +406,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def import_schema(self, request: Request, pk) -> HttpResponse:
|
def import_schema(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create operation with existing schema. '''
|
''' Create operation with existing schema. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.ImportSchemaSerializer(
|
serializer = s.ImportSchemaSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -465,9 +444,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create reference for operation',
|
summary='create replica for operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.CreateReferenceSerializer(),
|
request=s.CreateReplicaSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_201_CREATED: s.OperationCreatedResponse,
|
c.HTTP_201_CREATED: s.OperationCreatedResponse,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
|
@ -475,14 +454,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='create-reference')
|
@action(detail=True, methods=['post'], url_path='create-replica')
|
||||||
def create_reference(self, request: Request, pk) -> HttpResponse:
|
def create_replica(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Clone schema. '''
|
''' Replicate schema. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CreateReferenceSerializer(
|
serializer = s.CreateReplicaSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -490,7 +466,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oss = m.OperationSchema(item)
|
oss = m.OperationSchema(item)
|
||||||
target = cast(m.Operation, serializer.validated_data['target'])
|
target = cast(m.Operation, serializer.validated_data['target'])
|
||||||
new_operation = oss.create_reference(target)
|
new_operation = oss.create_replica(target)
|
||||||
layout.append({
|
layout.append({
|
||||||
'nodeID': 'o' + str(new_operation.pk),
|
'nodeID': 'o' + str(new_operation.pk),
|
||||||
'x': position['x'],
|
'x': position['x'],
|
||||||
|
|
@ -524,10 +500,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def create_synthesis(self, request: Request, pk) -> HttpResponse:
|
def create_synthesis(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create Synthesis operation from arguments. '''
|
''' Create Synthesis operation from arguments. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CreateSynthesisSerializer(
|
serializer = s.CreateSynthesisSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
position = serializer.validated_data['position']
|
position = serializer.validated_data['position']
|
||||||
|
|
@ -573,10 +546,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def update_operation(self, request: Request, pk) -> HttpResponse:
|
def update_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Update Operation arguments and parameters. '''
|
''' Update Operation arguments and parameters. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.UpdateOperationSerializer(
|
serializer = s.UpdateOperationSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
|
|
||||||
|
|
@ -628,10 +598,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete Operation. '''
|
''' Endpoint: Delete Operation. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.DeleteOperationSerializer(
|
serializer = s.DeleteOperationSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
old_schema = operation.result
|
old_schema = operation.result
|
||||||
|
|
@ -657,9 +624,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='delete reference',
|
summary='delete replica',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.DeleteReferenceSerializer(),
|
request=s.DeleteReplicaSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
|
@ -667,23 +634,22 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='delete-reference')
|
@action(detail=True, methods=['patch'], url_path='delete-replica')
|
||||||
def delete_reference(self, request: Request, pk) -> HttpResponse:
|
def delete_replica(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete Reference Operation. '''
|
''' Endpoint: Delete Replica Operation. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.DeleteReferenceSerializer(
|
serializer = s.DeleteReplicaSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
|
keep_connections = serializer.validated_data['keep_connections']
|
||||||
|
keep_constituents = serializer.validated_data['keep_constituents']
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oss = m.OperationSchemaCached(item)
|
oss = m.OperationSchemaCached(item)
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
oss.delete_reference(operation.pk, serializer.validated_data['keep_connections'])
|
oss.delete_replica(operation.pk, keep_connections, keep_constituents)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -706,10 +672,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def create_input(self, request: Request, pk) -> HttpResponse:
|
def create_input(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create input RSForm. '''
|
''' Create input RSForm. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.TargetOperationSerializer(
|
serializer = s.TargetOperationSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
if len(operation.getQ_arguments()) > 0:
|
if len(operation.getQ_arguments()) > 0:
|
||||||
|
|
@ -723,9 +686,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oss = m.OperationSchema(item)
|
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
schema = oss.create_input(operation)
|
schema = m.OperationSchema.create_input(item, operation)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -751,10 +713,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def set_input(self, request: Request, pk) -> HttpResponse:
|
def set_input(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Set input schema for target operation. '''
|
''' Set input schema for target operation. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.SetOperationInputSerializer(
|
serializer = s.SetOperationInputSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
|
|
@ -803,10 +762,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Execute operation. '''
|
''' Execute operation. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.TargetOperationSerializer(
|
serializer = s.TargetOperationSerializer(data=request.data, context={'oss': item})
|
||||||
data=request.data,
|
|
||||||
context={'oss': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
if operation.operation_type != m.OperationType.SYNTHESIS:
|
if operation.operation_type != m.OperationType.SYNTHESIS:
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class TestPromptTemplateViewSet(EndpointTester):
|
||||||
'text': 'prompt text',
|
'text': 'prompt text',
|
||||||
'is_shared': False
|
'is_shared': False
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
self.assertEqual(response.data['label'], 'Test')
|
self.assertEqual(response.data['label'], 'Test')
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ class TestPromptTemplateViewSet(EndpointTester):
|
||||||
'text': 'prompt text',
|
'text': 'prompt text',
|
||||||
'is_shared': True
|
'is_shared': True
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
self.assertTrue(response.data['is_shared'])
|
self.assertTrue(response.data['is_shared'])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,21 +50,21 @@ class TestPromptTemplateViewSet(EndpointTester):
|
||||||
'text': 'prompt text',
|
'text': 'prompt text',
|
||||||
'is_shared': True
|
'is_shared': True
|
||||||
}
|
}
|
||||||
response = self.executeBadData(data=data)
|
response = self.executeBadData(data)
|
||||||
self.assertIn('is_shared', response.data)
|
self.assertIn('is_shared', response.data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
||||||
def test_update_prompt_owner(self):
|
def test_update_prompt_owner(self):
|
||||||
prompt = PromptTemplate.objects.create(owner=self.user, label='ToUpdate', description='', text='t')
|
prompt = PromptTemplate.objects.create(owner=self.user, label='ToUpdate', description='', text='t')
|
||||||
response = self.executeOK(data={'label': 'Updated'}, item=prompt.id)
|
response = self.executeOK({'label': 'Updated'}, item=prompt.id)
|
||||||
self.assertEqual(response.data['label'], 'Updated')
|
self.assertEqual(response.data['label'], 'Updated')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
||||||
def test_update_prompt_not_owner_forbidden(self):
|
def test_update_prompt_not_owner_forbidden(self):
|
||||||
prompt = PromptTemplate.objects.create(owner=self.admin, label='Other', description='', text='t')
|
prompt = PromptTemplate.objects.create(owner=self.admin, label='Other', description='', text='t')
|
||||||
response = self.executeForbidden(data={'label': 'Updated'}, item=prompt.id)
|
response = self.executeForbidden({'label': 'Updated'}, item=prompt.id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/prompts/{item}/', method='delete')
|
@decl_endpoint('/api/prompts/{item}/', method='delete')
|
||||||
|
|
@ -112,4 +112,4 @@ class TestPromptTemplateViewSet(EndpointTester):
|
||||||
is_shared=True
|
is_shared=True
|
||||||
)
|
)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
response = self.executeForbidden(data={'label': 'Nope'}, item=prompt.id)
|
response = self.executeForbidden({'label': 'Nope'}, item=prompt.id)
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,11 @@ class ConstituentaAdmin(admin.ModelAdmin):
|
||||||
ordering = ['schema', 'order']
|
ordering = ['schema', 'order']
|
||||||
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
|
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
|
||||||
search_fields = ['term_resolved', 'definition_resolved']
|
search_fields = ['term_resolved', 'definition_resolved']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Attribution)
|
||||||
|
class AttributionAdmin(admin.ModelAdmin):
|
||||||
|
''' Admin model: Attribution. '''
|
||||||
|
ordering = ['container__schema', 'container', 'attribute']
|
||||||
|
list_display = ['container__schema__alias', 'container__alias', 'attribute__alias']
|
||||||
|
search_fields = ['container', 'attribute']
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ class Graph(Generic[ItemType]):
|
||||||
order = self.topological_order()
|
order = self.topological_order()
|
||||||
order.reverse()
|
order.reverse()
|
||||||
for node_id in order:
|
for node_id in order:
|
||||||
if len(self.inputs[node_id]) == 0:
|
if not self.inputs[node_id]:
|
||||||
continue
|
continue
|
||||||
for parent in self.inputs[node_id]:
|
for parent in self.inputs[node_id]:
|
||||||
result[parent] = result[parent] + [id for id in result[node_id] if id not in result[parent]]
|
result[parent] = result[parent] + [id for id in result[node_id] if id not in result[parent]]
|
||||||
|
|
@ -124,7 +124,7 @@ class Graph(Generic[ItemType]):
|
||||||
if node_id in marked:
|
if node_id in marked:
|
||||||
continue
|
continue
|
||||||
to_visit: list[ItemType] = [node_id]
|
to_visit: list[ItemType] = [node_id]
|
||||||
while len(to_visit) > 0:
|
while to_visit:
|
||||||
node = to_visit[-1]
|
node = to_visit[-1]
|
||||||
if node in marked:
|
if node in marked:
|
||||||
if node not in result:
|
if node not in result:
|
||||||
|
|
@ -132,7 +132,7 @@ class Graph(Generic[ItemType]):
|
||||||
to_visit.remove(node)
|
to_visit.remove(node)
|
||||||
else:
|
else:
|
||||||
marked.add(node)
|
marked.add(node)
|
||||||
if len(self.outputs[node]) <= 0:
|
if not self.outputs[node]:
|
||||||
continue
|
continue
|
||||||
for child_id in self.outputs[node]:
|
for child_id in self.outputs[node]:
|
||||||
if child_id not in marked:
|
if child_id not in marked:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-08-09 10:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rsform', '0004_constituenta_crucial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='constituenta',
|
||||||
|
name='cst_type',
|
||||||
|
field=models.CharField(choices=[('nominal', 'Nominal'), ('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Association',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('associate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_associate', to='rsform.constituenta', verbose_name='Ассоциированная конституента')),
|
||||||
|
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_container', to='rsform.constituenta', verbose_name='Составная конституента')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Ассоциация конституент',
|
||||||
|
'verbose_name_plural': 'Ассоциации конституент',
|
||||||
|
'unique_together': {('container', 'associate')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-08-21 11:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rsform', '0005_alter_constituenta_cst_type_association'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Attribution',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_attribute', to='rsform.constituenta', verbose_name='Атрибутирующая конституента')),
|
||||||
|
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_container', to='rsform.constituenta', verbose_name='Составная конституента')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Атрибутирование конституент',
|
||||||
|
'verbose_name_plural': 'Атрибутирования конституент',
|
||||||
|
'unique_together': {('container', 'attribute')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Association',
|
||||||
|
),
|
||||||
|
]
|
||||||
28
rsconcept/backend/apps/rsform/models/Attribution.py
Normal file
28
rsconcept/backend/apps/rsform/models/Attribution.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
''' Models: Attribution of nominal constituents. '''
|
||||||
|
from django.db.models import CASCADE, ForeignKey, Model
|
||||||
|
|
||||||
|
|
||||||
|
class Attribution(Model):
|
||||||
|
''' Attribution links nominal constituent to its content.'''
|
||||||
|
container = ForeignKey(
|
||||||
|
verbose_name='Составная конституента',
|
||||||
|
to='rsform.Constituenta',
|
||||||
|
on_delete=CASCADE,
|
||||||
|
related_name='as_container'
|
||||||
|
)
|
||||||
|
attribute = ForeignKey(
|
||||||
|
verbose_name='Атрибутирующая конституента',
|
||||||
|
to='rsform.Constituenta',
|
||||||
|
on_delete=CASCADE,
|
||||||
|
related_name='as_attribute'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
''' Model metadata. '''
|
||||||
|
verbose_name = 'Атрибутирование конституент'
|
||||||
|
verbose_name_plural = 'Атрибутирования конституент'
|
||||||
|
unique_together = [['container', 'attribute']]
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.container} -> {self.attribute}'
|
||||||
|
|
@ -16,9 +16,9 @@ from django.db.models import (
|
||||||
|
|
||||||
from ..utils import apply_pattern
|
from ..utils import apply_pattern
|
||||||
|
|
||||||
_RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line
|
_RE_GLOBALS = r'[XCSADFPTN]\d+' # cspell:disable-line
|
||||||
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
|
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
|
||||||
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
|
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPTN][0-9]+)') # cspell:disable-line
|
||||||
|
|
||||||
|
|
||||||
def extract_globals(expression: str) -> set[str]:
|
def extract_globals(expression: str) -> set[str]:
|
||||||
|
|
@ -38,6 +38,7 @@ def replace_entities(expression: str, mapping: dict[str, str]) -> str:
|
||||||
|
|
||||||
class CstType(TextChoices):
|
class CstType(TextChoices):
|
||||||
''' Type of constituenta. '''
|
''' Type of constituenta. '''
|
||||||
|
NOMINAL = 'nominal'
|
||||||
BASE = 'basic'
|
BASE = 'basic'
|
||||||
CONSTANT = 'constant'
|
CONSTANT = 'constant'
|
||||||
STRUCTURED = 'structure'
|
STRUCTURED = 'structure'
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class OrderManager:
|
||||||
continue
|
continue
|
||||||
result.append(cst)
|
result.append(cst)
|
||||||
children = self._semantic[cst.pk]['children']
|
children = self._semantic[cst.pk]['children']
|
||||||
if len(children) == 0:
|
if not children:
|
||||||
continue
|
continue
|
||||||
for child in self._items:
|
for child in self._items:
|
||||||
if child.pk in children:
|
if child.pk in children:
|
||||||
|
|
|
||||||
|
|
@ -158,12 +158,12 @@ class RSForm:
|
||||||
graph_terms = RSForm.graph_term(cst_list, cst_by_alias)
|
graph_terms = RSForm.graph_term(cst_list, cst_by_alias)
|
||||||
expansion = graph_terms.expand_outputs(changed)
|
expansion = graph_terms.expand_outputs(changed)
|
||||||
expanded_change = changed + expansion
|
expanded_change = changed + expansion
|
||||||
update_list: list[Constituenta] = []
|
|
||||||
|
|
||||||
if resolver is None:
|
if resolver is None:
|
||||||
resolver = RSForm.resolver_from_list(cst_list)
|
resolver = RSForm.resolver_from_list(cst_list)
|
||||||
|
|
||||||
if len(expansion) > 0:
|
if expansion:
|
||||||
|
resolved_terms: list[Constituenta] = []
|
||||||
for cst_id in graph_terms.topological_order():
|
for cst_id in graph_terms.topological_order():
|
||||||
if cst_id not in expansion:
|
if cst_id not in expansion:
|
||||||
continue
|
continue
|
||||||
|
|
@ -172,21 +172,20 @@ class RSForm:
|
||||||
if resolved == resolver.context[cst.alias].get_nominal():
|
if resolved == resolver.context[cst.alias].get_nominal():
|
||||||
continue
|
continue
|
||||||
cst.set_term_resolved(resolved)
|
cst.set_term_resolved(resolved)
|
||||||
update_list.append(cst)
|
resolved_terms.append(cst)
|
||||||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
Constituenta.objects.bulk_update(resolved_terms, ['term_resolved'])
|
||||||
|
|
||||||
graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
|
graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
|
||||||
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
|
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
|
||||||
update_list = []
|
if update_defs:
|
||||||
if len(update_defs) == 0:
|
resolved_defs: list[Constituenta] = []
|
||||||
return
|
for cst_id in update_defs:
|
||||||
for cst_id in update_defs:
|
cst = cst_by_id[cst_id]
|
||||||
cst = cst_by_id[cst_id]
|
resolved = resolver.resolve(cst.definition_raw)
|
||||||
resolved = resolver.resolve(cst.definition_raw)
|
cst.definition_resolved = resolved
|
||||||
cst.definition_resolved = resolved
|
resolved_defs.append(cst)
|
||||||
update_list.append(cst)
|
Constituenta.objects.bulk_update(resolved_defs, ['definition_resolved'])
|
||||||
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
|
|
||||||
|
|
||||||
def constituentsQ(self) -> QuerySet[Constituenta]:
|
def constituentsQ(self) -> QuerySet[Constituenta]:
|
||||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||||
|
|
@ -263,7 +262,7 @@ class RSForm:
|
||||||
|
|
||||||
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
||||||
''' Execute constituenta substitution. '''
|
''' Execute constituenta substitution. '''
|
||||||
if len(substitutions) < 1:
|
if not substitutions:
|
||||||
return
|
return
|
||||||
mapping = {}
|
mapping = {}
|
||||||
deleted: list[int] = []
|
deleted: list[int] = []
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class RSFormCached:
|
||||||
|
|
||||||
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
||||||
''' Execute constituenta substitution. '''
|
''' Execute constituenta substitution. '''
|
||||||
if len(substitutions) < 1:
|
if not substitutions:
|
||||||
return
|
return
|
||||||
self.cache.ensure_loaded_terms()
|
self.cache.ensure_loaded_terms()
|
||||||
mapping = {}
|
mapping = {}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class SemanticInfo:
|
||||||
return sources
|
return sources
|
||||||
|
|
||||||
def _need_check_head(self, sources: set[int], head: str) -> bool:
|
def _need_check_head(self, sources: set[int], head: str) -> bool:
|
||||||
if len(sources) == 0:
|
if not sources:
|
||||||
return True
|
return True
|
||||||
elif len(sources) != 1:
|
elif len(sources) != 1:
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
''' Django: Models. '''
|
''' Django: Models. '''
|
||||||
|
|
||||||
|
from .Attribution import Attribution
|
||||||
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
||||||
from .OrderManager import OrderManager
|
from .OrderManager import OrderManager
|
||||||
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ def get_type_prefix(cst_type: str) -> str:
|
||||||
case CstType.FUNCTION: return 'F'
|
case CstType.FUNCTION: return 'F'
|
||||||
case CstType.PREDICATE: return 'P'
|
case CstType.PREDICATE: return 'P'
|
||||||
case CstType.THEOREM: return 'T'
|
case CstType.THEOREM: return 'T'
|
||||||
|
case CstType.NOMINAL: return 'N'
|
||||||
return 'X'
|
return 'X'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,7 +79,7 @@ def guess_type(alias: str) -> CstType:
|
||||||
def _get_structure_prefix(alias: str, expression: str, parse: dict) -> Tuple[str, str]:
|
def _get_structure_prefix(alias: str, expression: str, parse: dict) -> Tuple[str, str]:
|
||||||
''' Generate prefix and alias for structure generation. '''
|
''' Generate prefix and alias for structure generation. '''
|
||||||
args = parse['args']
|
args = parse['args']
|
||||||
if len(args) == 0:
|
if not args:
|
||||||
return (alias, '')
|
return (alias, '')
|
||||||
prefix = expression[0:expression.find(']')] + '] '
|
prefix = expression[0:expression.find(']')] + '] '
|
||||||
newAlias = alias + '[' + ','.join([arg['alias'] for arg in args]) + ']'
|
newAlias = alias + '[' + ','.join([arg['alias'] for arg in args]) + ']'
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ from .basics import (
|
||||||
WordFormSerializer
|
WordFormSerializer
|
||||||
)
|
)
|
||||||
from .data_access import (
|
from .data_access import (
|
||||||
|
AttributionCreateSerializer,
|
||||||
|
AttributionDataSerializer,
|
||||||
CrucialUpdateSerializer,
|
CrucialUpdateSerializer,
|
||||||
CstCreateSerializer,
|
CstCreateSerializer,
|
||||||
CstInfoSerializer,
|
CstInfoSerializer,
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class ReferenceSerializer(StrictSerializer):
|
||||||
|
|
||||||
|
|
||||||
class InheritanceDataSerializer(StrictSerializer):
|
class InheritanceDataSerializer(StrictSerializer):
|
||||||
''' Serializer: inheritance data. '''
|
''' Serializer: Inheritance data. '''
|
||||||
child = serializers.IntegerField()
|
child = serializers.IntegerField()
|
||||||
child_source = serializers.IntegerField()
|
child_source = serializers.IntegerField()
|
||||||
parent = serializers.IntegerField() # type: ignore
|
parent = serializers.IntegerField() # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,55 @@ from apps.oss.models import Inheritance
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared.serializers import StrictModelSerializer, StrictSerializer
|
from shared.serializers import StrictModelSerializer, StrictSerializer
|
||||||
|
|
||||||
from ..models import Constituenta, CstType, RSForm
|
from ..models import Attribution, Constituenta, CstType, RSForm
|
||||||
from .basics import CstParseSerializer, InheritanceDataSerializer
|
from .basics import CstParseSerializer, InheritanceDataSerializer
|
||||||
from .io_pyconcept import PyConceptAdapter
|
from .io_pyconcept import PyConceptAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionSerializer(StrictModelSerializer):
|
||||||
|
''' Serializer: Attribution relation. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = Attribution
|
||||||
|
fields = ('container', 'attribute')
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionDataSerializer(StrictSerializer):
|
||||||
|
''' Serializer: Attribution data. '''
|
||||||
|
container = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
|
||||||
|
attribute = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
schema = cast(LibraryItem, self.context['schema'])
|
||||||
|
if schema and attrs['container'].schema_id != schema.id:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'container': msg.constituentaNotInRSform(schema.title)
|
||||||
|
})
|
||||||
|
if schema and attrs['attribute'].schema_id != schema.id:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'attribute': msg.constituentaNotInRSform(schema.title)
|
||||||
|
})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionCreateSerializer(AttributionDataSerializer):
|
||||||
|
''' Serializer: Data for creating new Attribution. '''
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
if attrs['container'].pk == attrs['attribute'].pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'container': msg.associationSelf()
|
||||||
|
})
|
||||||
|
if Attribution.objects.filter(container=attrs['container'], attribute=attrs['attribute']).exists():
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'attribute': msg.associationAlreadyExists()
|
||||||
|
})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class CstBaseSerializer(StrictModelSerializer):
|
class CstBaseSerializer(StrictModelSerializer):
|
||||||
''' Serializer: Constituenta all data. '''
|
''' Serializer: Constituenta all data. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -122,6 +166,15 @@ class CstCreateSerializer(StrictModelSerializer):
|
||||||
'term_raw', 'definition_raw', 'definition_formal', \
|
'term_raw', 'definition_raw', 'definition_formal', \
|
||||||
'insert_after', 'term_forms'
|
'insert_after', 'term_forms'
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
schema = cast(LibraryItem, self.context['schema'])
|
||||||
|
insert_after = attrs.get('insert_after')
|
||||||
|
if insert_after and insert_after.schema_id != schema.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'insert_after': msg.constituentaNotInRSform(schema.title)
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class RSFormSerializer(StrictModelSerializer):
|
class RSFormSerializer(StrictModelSerializer):
|
||||||
''' Serializer: Detailed data for RSForm. '''
|
''' Serializer: Detailed data for RSForm. '''
|
||||||
|
|
@ -134,6 +187,9 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
inheritance = serializers.ListField(
|
inheritance = serializers.ListField(
|
||||||
child=InheritanceDataSerializer()
|
child=InheritanceDataSerializer()
|
||||||
)
|
)
|
||||||
|
attribution = serializers.ListField(
|
||||||
|
child=AttributionSerializer()
|
||||||
|
)
|
||||||
oss = serializers.ListField(
|
oss = serializers.ListField(
|
||||||
child=LibraryItemReferenceSerializer()
|
child=LibraryItemReferenceSerializer()
|
||||||
)
|
)
|
||||||
|
|
@ -164,6 +220,7 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
result['items'] = []
|
result['items'] = []
|
||||||
result['oss'] = []
|
result['oss'] = []
|
||||||
result['inheritance'] = []
|
result['inheritance'] = []
|
||||||
|
result['attribution'] = []
|
||||||
for cst in Constituenta.objects.filter(schema=instance).defer('order').order_by('order'):
|
for cst in Constituenta.objects.filter(schema=instance).defer('order').order_by('order'):
|
||||||
result['items'].append(CstInfoSerializer(cst).data)
|
result['items'].append(CstInfoSerializer(cst).data)
|
||||||
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
||||||
|
|
@ -171,6 +228,11 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
'id': oss.pk,
|
'id': oss.pk,
|
||||||
'alias': oss.alias
|
'alias': oss.alias
|
||||||
})
|
})
|
||||||
|
for assoc in Attribution.objects.filter(container__schema=instance).only('container_id', 'attribute_id'):
|
||||||
|
result['attribution'].append({
|
||||||
|
'container': assoc.container_id,
|
||||||
|
'attribute': assoc.attribute_id
|
||||||
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def to_versioned_data(self) -> dict:
|
def to_versioned_data(self) -> dict:
|
||||||
|
|
@ -200,37 +262,38 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
instance = cast(LibraryItem, self.instance)
|
instance = cast(LibraryItem, self.instance)
|
||||||
schema = RSForm(instance)
|
schema = RSForm(instance)
|
||||||
items: list[dict] = data['items']
|
items: list[dict] = data['items']
|
||||||
ids: list[int] = [item['id'] for item in items]
|
stored_ids: list[int] = [item['id'] for item in items]
|
||||||
processed: list[int] = []
|
id_map: dict[int, int] = {}
|
||||||
|
|
||||||
for cst in schema.constituentsQ():
|
for existing_cst in schema.constituentsQ():
|
||||||
if not cst.pk in ids:
|
if not existing_cst.pk in stored_ids:
|
||||||
cst.delete()
|
existing_cst.delete()
|
||||||
else:
|
else:
|
||||||
cst_data = next(x for x in items if x['id'] == cst.pk)
|
cst_data = next(x for x in items if x['id'] == existing_cst.pk)
|
||||||
cst_data['schema'] = instance.pk
|
cst_data['schema'] = instance.pk
|
||||||
new_cst = CstBaseSerializer(data=cst_data)
|
cst_serializer = CstBaseSerializer(data=cst_data)
|
||||||
new_cst.is_valid(raise_exception=True)
|
cst_serializer.is_valid(raise_exception=True)
|
||||||
new_cst.validated_data['order'] = ids.index(cst.pk)
|
cst_serializer.validated_data['order'] = stored_ids.index(existing_cst.pk)
|
||||||
new_cst.update(
|
cst_serializer.update(
|
||||||
instance=cst,
|
instance=existing_cst,
|
||||||
validated_data=new_cst.validated_data
|
validated_data=cst_serializer.validated_data
|
||||||
)
|
)
|
||||||
processed.append(cst.pk)
|
id_map[cst_data['id']] = existing_cst.pk
|
||||||
|
|
||||||
for cst_data in items:
|
for cst_data in items:
|
||||||
if cst_data['id'] not in processed:
|
if cst_data['id'] not in id_map:
|
||||||
cst = schema.insert_last(cst_data['alias'])
|
|
||||||
old_id = cst_data['id']
|
old_id = cst_data['id']
|
||||||
cst_data['id'] = cst.pk
|
inserted_cst = schema.insert_last(cst_data['alias'])
|
||||||
|
cst_data['id'] = inserted_cst.pk
|
||||||
cst_data['schema'] = instance.pk
|
cst_data['schema'] = instance.pk
|
||||||
new_cst = CstBaseSerializer(data=cst_data)
|
cst_serializer = CstBaseSerializer(data=cst_data)
|
||||||
new_cst.is_valid(raise_exception=True)
|
cst_serializer.is_valid(raise_exception=True)
|
||||||
new_cst.validated_data['order'] = ids.index(old_id)
|
cst_serializer.validated_data['order'] = stored_ids.index(old_id)
|
||||||
new_cst.update(
|
cst_serializer.update(
|
||||||
instance=cst,
|
instance=inserted_cst,
|
||||||
validated_data=new_cst.validated_data
|
validated_data=cst_serializer.validated_data
|
||||||
)
|
)
|
||||||
|
id_map[old_id] = inserted_cst.pk
|
||||||
|
|
||||||
loaded_item = LibraryItemBaseNonStrictSerializer(data=data)
|
loaded_item = LibraryItemBaseNonStrictSerializer(data=data)
|
||||||
loaded_item.is_valid(raise_exception=True)
|
loaded_item.is_valid(raise_exception=True)
|
||||||
|
|
@ -239,6 +302,23 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
validated_data=loaded_item.validated_data
|
validated_data=loaded_item.validated_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Attribution.objects.filter(container__schema=instance).delete()
|
||||||
|
attributions_to_create: list[Attribution] = []
|
||||||
|
for assoc in data.get('attribution', []):
|
||||||
|
old_container_id = assoc['container']
|
||||||
|
old_attribute_id = assoc['attribute']
|
||||||
|
container_id = id_map.get(old_container_id)
|
||||||
|
attribute_id = id_map.get(old_attribute_id)
|
||||||
|
if container_id and attribute_id:
|
||||||
|
attributions_to_create.append(
|
||||||
|
Attribution(
|
||||||
|
container_id=container_id,
|
||||||
|
attribute_id=attribute_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if attributions_to_create:
|
||||||
|
Attribution.objects.bulk_create(attributions_to_create)
|
||||||
|
|
||||||
|
|
||||||
class RSFormParseSerializer(StrictModelSerializer):
|
class RSFormParseSerializer(StrictModelSerializer):
|
||||||
''' Serializer: Detailed data for RSForm including parse. '''
|
''' Serializer: Detailed data for RSForm including parse. '''
|
||||||
|
|
@ -267,6 +347,8 @@ class RSFormParseSerializer(StrictModelSerializer):
|
||||||
def _parse_data(self, data: dict) -> dict:
|
def _parse_data(self, data: dict) -> dict:
|
||||||
parse = PyConceptAdapter(data).parse()
|
parse = PyConceptAdapter(data).parse()
|
||||||
for cst_data in data['items']:
|
for cst_data in data['items']:
|
||||||
|
if cst_data['cst_type'] == CstType.NOMINAL:
|
||||||
|
continue
|
||||||
cst_data['parse'] = next(
|
cst_data['parse'] = next(
|
||||||
cst['parse'] for cst in parse['items']
|
cst['parse'] for cst in parse['items']
|
||||||
if cst['id'] == cst_data['id']
|
if cst['id'] == cst_data['id']
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ from apps.library.models import LibraryItem
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared.serializers import StrictSerializer
|
from shared.serializers import StrictSerializer
|
||||||
|
|
||||||
from ..models import Constituenta, RSFormCached
|
from ..models import Constituenta, CstType, RSFormCached
|
||||||
from ..utils import fix_old_references
|
from ..utils import fix_old_references
|
||||||
|
|
||||||
_CST_TYPE = 'constituenta'
|
_ENTITY_CONSTITUENTA = 'constituenta'
|
||||||
_TRS_TYPE = 'rsform'
|
_ENTITY_SCHEMA = 'rsform'
|
||||||
_TRS_VERSION_MIN = 16
|
_TRS_VERSION_MIN = 16
|
||||||
_TRS_VERSION = 16
|
_TRS_VERSION = 16
|
||||||
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
|
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
|
||||||
|
|
@ -30,11 +30,11 @@ class RSFormUploadSerializer(StrictSerializer):
|
||||||
def generate_trs(schema: LibraryItem) -> dict:
|
def generate_trs(schema: LibraryItem) -> dict:
|
||||||
''' Generate TRS file for RSForm. '''
|
''' Generate TRS file for RSForm. '''
|
||||||
items = []
|
items = []
|
||||||
for cst in Constituenta.objects.filter(schema=schema).order_by('order'):
|
for cst in Constituenta.objects.filter(schema=schema).exclude(cst_type=CstType.NOMINAL).order_by('order'):
|
||||||
items.append(
|
items.append(
|
||||||
{
|
{
|
||||||
'entityUID': cst.pk,
|
'entityUID': cst.pk,
|
||||||
'type': _CST_TYPE,
|
'type': _ENTITY_CONSTITUENTA,
|
||||||
'cstType': cst.cst_type,
|
'cstType': cst.cst_type,
|
||||||
'alias': cst.alias,
|
'alias': cst.alias,
|
||||||
'convention': cst.convention,
|
'convention': cst.convention,
|
||||||
|
|
@ -53,7 +53,7 @@ def generate_trs(schema: LibraryItem) -> dict:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
'type': _TRS_TYPE,
|
'type': _ENTITY_SCHEMA,
|
||||||
'title': schema.title,
|
'title': schema.title,
|
||||||
'alias': schema.alias,
|
'alias': schema.alias,
|
||||||
'comment': schema.description,
|
'comment': schema.description,
|
||||||
|
|
@ -72,7 +72,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
def load_versioned_data(data: dict) -> dict:
|
def load_versioned_data(data: dict) -> dict:
|
||||||
''' Load data from version. '''
|
''' Load data from version. '''
|
||||||
result = {
|
result = {
|
||||||
'type': _TRS_TYPE,
|
'type': _ENTITY_SCHEMA,
|
||||||
'title': data['title'],
|
'title': data['title'],
|
||||||
'alias': data['alias'],
|
'alias': data['alias'],
|
||||||
'comment': data['description'],
|
'comment': data['description'],
|
||||||
|
|
@ -85,7 +85,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
for cst in data['items']:
|
for cst in data['items']:
|
||||||
result['items'].append({
|
result['items'].append({
|
||||||
'entityUID': cst['id'],
|
'entityUID': cst['id'],
|
||||||
'type': _CST_TYPE,
|
'type': _ENTITY_CONSTITUENTA,
|
||||||
'cstType': cst['cst_type'],
|
'cstType': cst['cst_type'],
|
||||||
'alias': cst['alias'],
|
'alias': cst['alias'],
|
||||||
'convention': cst['convention'],
|
'convention': cst['convention'],
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import pyconcept
|
||||||
|
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
|
|
||||||
from ..models import Constituenta
|
from ..models import Constituenta, CstType
|
||||||
|
|
||||||
|
|
||||||
class PyConceptAdapter:
|
class PyConceptAdapter:
|
||||||
|
|
@ -34,7 +34,7 @@ class PyConceptAdapter:
|
||||||
result: dict = {
|
result: dict = {
|
||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
items = Constituenta.objects.filter(schema_id=schemaID).order_by('order')
|
items = Constituenta.objects.filter(schema_id=schemaID).exclude(cst_type=CstType.NOMINAL).order_by('order')
|
||||||
for cst in items:
|
for cst in items:
|
||||||
result['items'].append({
|
result['items'].append({
|
||||||
'entityUID': cst.pk,
|
'entityUID': cst.pk,
|
||||||
|
|
@ -51,6 +51,8 @@ class PyConceptAdapter:
|
||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
for cst in data['items']:
|
for cst in data['items']:
|
||||||
|
if cst['cst_type'] == CstType.NOMINAL:
|
||||||
|
continue
|
||||||
result['items'].append({
|
result['items'].append({
|
||||||
'entityUID': cst['id'],
|
'entityUID': cst['id'],
|
||||||
'cstType': cst['cst_type'],
|
'cstType': cst['cst_type'],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
''' Tests for Django Models. '''
|
''' Tests for Django Models. '''
|
||||||
|
from .t_Attribution import *
|
||||||
from .t_Constituenta import *
|
from .t_Constituenta import *
|
||||||
from .t_RSForm import *
|
from .t_RSForm import *
|
||||||
from .t_RSFormCached import *
|
from .t_RSFormCached import *
|
||||||
|
|
|
||||||
175
rsconcept/backend/apps/rsform/tests/s_models/t_Attribution.py
Normal file
175
rsconcept/backend/apps/rsform/tests/s_models/t_Attribution.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
''' Testing models: Attribution. '''
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from apps.rsform.models import Attribution, Constituenta, CstType, RSForm
|
||||||
|
|
||||||
|
|
||||||
|
class TestAttribution(TestCase):
|
||||||
|
''' Testing Attribution model. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.schema = RSForm.create(title='Test1')
|
||||||
|
|
||||||
|
# Create test constituents
|
||||||
|
self.container1 = Constituenta.objects.create(
|
||||||
|
alias='C1',
|
||||||
|
schema=self.schema.model,
|
||||||
|
order=1,
|
||||||
|
cst_type=CstType.NOMINAL
|
||||||
|
)
|
||||||
|
self.attribute1 = Constituenta.objects.create(
|
||||||
|
alias='A1',
|
||||||
|
schema=self.schema.model,
|
||||||
|
order=2,
|
||||||
|
cst_type=CstType.BASE
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
''' Test string representation. '''
|
||||||
|
attribution = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
expected_str = f'{self.container1} -> {self.attribute1}'
|
||||||
|
self.assertEqual(str(attribution), expected_str)
|
||||||
|
|
||||||
|
def test_create_attribution(self):
|
||||||
|
''' Test basic Attribution creation. '''
|
||||||
|
attr = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
self.assertEqual(attr.container, self.container1)
|
||||||
|
self.assertEqual(attr.attribute, self.attribute1)
|
||||||
|
self.assertIsNotNone(attr.id)
|
||||||
|
|
||||||
|
def test_unique_constraint(self):
|
||||||
|
''' Test unique constraint on container and attribute. '''
|
||||||
|
# Create first Attribution
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to create duplicate Attribution
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_container_not_null(self):
|
||||||
|
''' Test container field cannot be null. '''
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=None,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_attribute_not_null(self):
|
||||||
|
''' Test attribute field cannot be null. '''
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cascade_delete_container(self):
|
||||||
|
''' Test cascade delete when container is deleted. '''
|
||||||
|
attribution = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
association_id = attribution.id
|
||||||
|
|
||||||
|
# Delete the container
|
||||||
|
self.container1.delete()
|
||||||
|
|
||||||
|
# Attribution should be deleted due to CASCADE
|
||||||
|
with self.assertRaises(Attribution.DoesNotExist):
|
||||||
|
Attribution.objects.get(id=association_id)
|
||||||
|
|
||||||
|
def test_cascade_delete_attribute(self):
|
||||||
|
''' Test cascade delete when attribute is deleted. '''
|
||||||
|
attribution = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
association_id = attribution.id
|
||||||
|
|
||||||
|
# Delete the attribute
|
||||||
|
self.attribute1.delete()
|
||||||
|
|
||||||
|
# Attribution should be deleted due to CASCADE
|
||||||
|
with self.assertRaises(Attribution.DoesNotExist):
|
||||||
|
Attribution.objects.get(id=association_id)
|
||||||
|
|
||||||
|
def test_related_names(self):
|
||||||
|
''' Test related names for foreign key relationships. '''
|
||||||
|
attribution = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test container related name
|
||||||
|
container_associations = self.container1.as_container.all()
|
||||||
|
self.assertEqual(len(container_associations), 1)
|
||||||
|
self.assertEqual(container_associations[0], attribution)
|
||||||
|
|
||||||
|
# Test attribute related name
|
||||||
|
attribute_associations = self.attribute1.as_attribute.all()
|
||||||
|
self.assertEqual(len(attribute_associations), 1)
|
||||||
|
self.assertEqual(attribute_associations[0], attribution)
|
||||||
|
|
||||||
|
def test_multiple_attributions_same_container(self):
|
||||||
|
''' Test multiple Attributions with same container. '''
|
||||||
|
attribute3 = Constituenta.objects.create(
|
||||||
|
alias='A3',
|
||||||
|
schema=self.schema.model,
|
||||||
|
order=3,
|
||||||
|
cst_type=CstType.BASE
|
||||||
|
)
|
||||||
|
|
||||||
|
attr1 = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
attr2 = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=attribute3
|
||||||
|
)
|
||||||
|
|
||||||
|
container_associations = self.container1.as_container.all()
|
||||||
|
self.assertEqual(len(container_associations), 2)
|
||||||
|
self.assertIn(attr1, container_associations)
|
||||||
|
self.assertIn(attr2, container_associations)
|
||||||
|
|
||||||
|
def test_multiple_attributions_same_attribute(self):
|
||||||
|
''' Test multiple Attributions with same attribute. '''
|
||||||
|
container3 = Constituenta.objects.create(
|
||||||
|
alias='C3',
|
||||||
|
schema=self.schema.model,
|
||||||
|
order=3,
|
||||||
|
cst_type=CstType.NOMINAL
|
||||||
|
)
|
||||||
|
|
||||||
|
attr1 = Attribution.objects.create(
|
||||||
|
container=self.container1,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
attr2 = Attribution.objects.create(
|
||||||
|
container=container3,
|
||||||
|
attribute=self.attribute1
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_associations = self.attribute1.as_attribute.all()
|
||||||
|
self.assertEqual(len(attribute_associations), 2)
|
||||||
|
self.assertIn(attr1, attribute_associations)
|
||||||
|
self.assertIn(attr2, attribute_associations)
|
||||||
|
|
||||||
|
def test_meta_unique_together(self):
|
||||||
|
''' Test Meta class unique_together constraint. '''
|
||||||
|
unique_together = Attribution._meta.unique_together
|
||||||
|
self.assertEqual(len(unique_together), 1)
|
||||||
|
self.assertIn(('container', 'attribute'), unique_together)
|
||||||
|
|
@ -48,7 +48,7 @@ class TestRSFormCached(DBTester):
|
||||||
|
|
||||||
x1 = self.schema.insert_last('X1')
|
x1 = self.schema.insert_last('X1')
|
||||||
x2 = self.schema.insert_last('X2')
|
x2 = self.schema.insert_last('X2')
|
||||||
x3 = self.schema.create_cst(data=data, insert_after=x1)
|
x3 = self.schema.create_cst(data, insert_after=x1)
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(x3.alias, data['alias'])
|
self.assertEqual(x3.alias, data['alias'])
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
''' Tests for REST API. '''
|
''' Tests for REST API. '''
|
||||||
|
from .t_attribtuions import *
|
||||||
from .t_cctext import *
|
from .t_cctext import *
|
||||||
|
from .t_constituenta import *
|
||||||
from .t_rsforms import *
|
from .t_rsforms import *
|
||||||
from .t_rslang import *
|
from .t_rslang import *
|
||||||
|
|
|
||||||
100
rsconcept/backend/apps/rsform/tests/s_views/t_attribtuions.py
Normal file
100
rsconcept/backend/apps/rsform/tests/s_views/t_attribtuions.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
''' Testing API: Attribution. '''
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from cctext import ReferenceType
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||||
|
from apps.rsform.models import Attribution, Constituenta, CstType, RSForm
|
||||||
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
from shared.testing_utils import response_contains
|
||||||
|
|
||||||
|
|
||||||
|
class TestAttributionsEndpoints(EndpointTester):
|
||||||
|
''' Testing basic Attribution API. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
|
||||||
|
self.owned_id = self.owned.model.pk
|
||||||
|
self.unowned = RSForm.create(title='Test2', alias='T2')
|
||||||
|
self.unowned_id = self.unowned.model.pk
|
||||||
|
self.n1 = self.owned.insert_last('N1')
|
||||||
|
self.x1 = self.owned.insert_last('X1')
|
||||||
|
self.n2 = self.owned.insert_last('N2')
|
||||||
|
self.unowned_cst = self.unowned.insert_last('C1')
|
||||||
|
self.invalid_id = self.n2.pk + 1337
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
||||||
|
def test_create_attribution(self):
|
||||||
|
self.executeBadData({}, item=self.owned_id)
|
||||||
|
|
||||||
|
data = {'container': self.n1.pk, 'attribute': self.invalid_id}
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['attribute'] = self.unowned_cst.pk
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['attribute'] = data['container']
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
|
||||||
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
|
associations = response.data['attribution']
|
||||||
|
self.assertEqual(len(associations), 1)
|
||||||
|
self.assertEqual(associations[0]['container'], self.n1.pk)
|
||||||
|
self.assertEqual(associations[0]['attribute'], self.x1.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
||||||
|
def test_create_attribution_duplicate(self):
|
||||||
|
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
|
||||||
|
self.executeCreated(data, item=self.owned_id)
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/delete-attribution', method='patch')
|
||||||
|
def test_delete_attribution(self):
|
||||||
|
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.n1,
|
||||||
|
attribute=self.x1
|
||||||
|
)
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
|
attributions = response.data['attribution']
|
||||||
|
self.assertEqual(len(attributions), 0)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/clear-attributions', method='patch')
|
||||||
|
def test_clear_attributions(self):
|
||||||
|
data = {'target': self.n1.pk}
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
self.executeNotFound(data, item=self.invalid_id)
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.n1,
|
||||||
|
attribute=self.x1
|
||||||
|
)
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.n1,
|
||||||
|
attribute=self.n2
|
||||||
|
)
|
||||||
|
Attribution.objects.create(
|
||||||
|
container=self.n2,
|
||||||
|
attribute=self.n1
|
||||||
|
)
|
||||||
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
|
associations = response.data['attribution']
|
||||||
|
self.assertEqual(len(associations), 1)
|
||||||
|
self.assertEqual(associations[0]['container'], self.n2.pk)
|
||||||
|
self.assertEqual(associations[0]['attribute'], self.n1.pk)
|
||||||
|
|
@ -14,20 +14,20 @@ class TestNaturalLanguageViews(EndpointTester):
|
||||||
@decl_endpoint(endpoint='/api/cctext/parse', method='post')
|
@decl_endpoint(endpoint='/api/cctext/parse', method='post')
|
||||||
def test_parse_text(self):
|
def test_parse_text(self):
|
||||||
data = {'text': 'синим слонам'}
|
data = {'text': 'синим слонам'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc')
|
self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint(endpoint='/api/cctext/inflect', method='post')
|
@decl_endpoint(endpoint='/api/cctext/inflect', method='post')
|
||||||
def test_inflect(self):
|
def test_inflect(self):
|
||||||
data = {'text': 'синий слон', 'grams': 'plur,datv'}
|
data = {'text': 'синий слон', 'grams': 'plur,datv'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertEqual(response.data['result'], 'синим слонам')
|
self.assertEqual(response.data['result'], 'синим слонам')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint(endpoint='/api/cctext/generate-lexeme', method='post')
|
@decl_endpoint(endpoint='/api/cctext/generate-lexeme', method='post')
|
||||||
def test_generate_lexeme(self):
|
def test_generate_lexeme(self):
|
||||||
data = {'text': 'синий слон'}
|
data = {'text': 'синий слон'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertEqual(len(response.data['items']), 12)
|
self.assertEqual(len(response.data['items']), 12)
|
||||||
self.assertEqual(response.data['items'][0]['text'], 'синий слон')
|
self.assertEqual(response.data['items'][0]['text'], 'синий слон')
|
||||||
|
|
|
||||||
195
rsconcept/backend/apps/rsform/tests/s_views/t_constituenta.py
Normal file
195
rsconcept/backend/apps/rsform/tests/s_views/t_constituenta.py
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
''' Testing API: Constituenta editing. '''
|
||||||
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstituentaAPI(EndpointTester):
|
||||||
|
''' Testing Constituenta view. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
|
||||||
|
self.owned_id = self.owned.model.pk
|
||||||
|
self.unowned = RSForm.create(title='Test2', alias='T2')
|
||||||
|
self.unowned_id = self.unowned.model.pk
|
||||||
|
self.x1 = Constituenta.objects.create(
|
||||||
|
alias='X1',
|
||||||
|
cst_type=CstType.BASE,
|
||||||
|
schema=self.owned.model,
|
||||||
|
order=0,
|
||||||
|
convention='Test',
|
||||||
|
term_raw='Test1',
|
||||||
|
term_resolved='Test1R',
|
||||||
|
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
|
||||||
|
self.x2 = Constituenta.objects.create(
|
||||||
|
alias='X2',
|
||||||
|
cst_type=CstType.BASE,
|
||||||
|
schema=self.owned.model,
|
||||||
|
order=1,
|
||||||
|
convention='Test1',
|
||||||
|
term_raw='Test2',
|
||||||
|
term_resolved='Test2R'
|
||||||
|
)
|
||||||
|
self.x3 = Constituenta.objects.create(
|
||||||
|
alias='X3',
|
||||||
|
schema=self.owned.model,
|
||||||
|
order=2,
|
||||||
|
term_raw='Test3',
|
||||||
|
term_resolved='Test3',
|
||||||
|
definition_raw='Test1',
|
||||||
|
definition_resolved='Test2'
|
||||||
|
)
|
||||||
|
self.unowned_cst = self.unowned.insert_last(alias='X1', cst_type=CstType.BASE)
|
||||||
|
self.invalid_cst = self.x3.pk + 1337
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||||
|
def test_partial_update(self):
|
||||||
|
data = {'target': self.x1.pk, 'item_data': {'convention': 'tt'}}
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
self.logout()
|
||||||
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
self.executeOK(data)
|
||||||
|
self.x1.refresh_from_db()
|
||||||
|
self.assertEqual(self.x1.convention, 'tt')
|
||||||
|
|
||||||
|
self.executeOK(data)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||||
|
def test_partial_update_rename(self):
|
||||||
|
data = {'target': self.x1.pk, 'item_data': {'alias': self.x3.alias}}
|
||||||
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
d1 = self.owned.insert_last(
|
||||||
|
alias='D1',
|
||||||
|
term_raw='@{X1|plur}',
|
||||||
|
definition_formal='X1'
|
||||||
|
)
|
||||||
|
self.assertEqual(self.x1.order, 0)
|
||||||
|
self.assertEqual(self.x1.alias, 'X1')
|
||||||
|
self.assertEqual(self.x1.cst_type, CstType.BASE)
|
||||||
|
|
||||||
|
data = {'target': self.x1.pk, 'item_data': {'alias': 'D2', 'cst_type': CstType.TERM}}
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
d1.refresh_from_db()
|
||||||
|
self.x1.refresh_from_db()
|
||||||
|
self.assertEqual(d1.term_resolved, '')
|
||||||
|
self.assertEqual(d1.term_raw, '@{D2|plur}')
|
||||||
|
self.assertEqual(self.x1.order, 0)
|
||||||
|
self.assertEqual(self.x1.alias, 'D2')
|
||||||
|
self.assertEqual(self.x1.cst_type, CstType.TERM)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||||
|
def test_update_resolved_no_refs(self):
|
||||||
|
data = {
|
||||||
|
'target': self.x3.pk,
|
||||||
|
'item_data': {
|
||||||
|
'term_raw': 'New term',
|
||||||
|
'definition_raw': 'New def'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.x3.refresh_from_db()
|
||||||
|
self.assertEqual(self.x3.term_resolved, 'New term')
|
||||||
|
self.assertEqual(self.x3.definition_resolved, 'New def')
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||||
|
def test_update_resolved_refs(self):
|
||||||
|
data = {
|
||||||
|
'target': self.x3.pk,
|
||||||
|
'item_data': {
|
||||||
|
'term_raw': '@{X1|nomn,sing}',
|
||||||
|
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.x3.refresh_from_db()
|
||||||
|
self.assertEqual(self.x3.term_resolved, self.x1.term_resolved)
|
||||||
|
self.assertEqual(self.x3.definition_resolved, f'{self.x1.term_resolved} form1')
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||||
|
def test_update_term_forms(self):
|
||||||
|
data = {
|
||||||
|
'target': self.x3.pk,
|
||||||
|
'item_data': {
|
||||||
|
'definition_raw': '@{X3|sing,datv}',
|
||||||
|
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.x3.refresh_from_db()
|
||||||
|
self.assertEqual(self.x3.definition_resolved, 'form1')
|
||||||
|
self.assertEqual(self.x3.term_forms, data['item_data']['term_forms'])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/update-crucial', method='patch')
|
||||||
|
def test_update_crucial(self):
|
||||||
|
data = {'target': [self.x1.pk], 'value': True}
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
self.logout()
|
||||||
|
self.executeForbidden(data, item=self.owned_id)
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
self.executeOK(data, item=self.owned_id)
|
||||||
|
self.x1.refresh_from_db()
|
||||||
|
self.assertEqual(self.x1.crucial, True)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
||||||
|
def test_create_constituenta(self):
|
||||||
|
data = {'alias': 'X4', 'cst_type': CstType.BASE}
|
||||||
|
self.executeForbidden(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
data = {'alias': 'X4'}
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
data['cst_type'] = 'invalid'
|
||||||
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'alias': 'X4',
|
||||||
|
'cst_type': CstType.BASE,
|
||||||
|
'term_raw': 'test',
|
||||||
|
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
|
||||||
|
'definition_formal': 'invalid',
|
||||||
|
'crucial': True
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data)
|
||||||
|
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
||||||
|
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
|
||||||
|
self.assertEqual(x4.order, 3)
|
||||||
|
self.assertEqual(x4.term_raw, data['term_raw'])
|
||||||
|
self.assertEqual(x4.term_forms, data['term_forms'])
|
||||||
|
self.assertEqual(x4.definition_formal, data['definition_formal'])
|
||||||
|
self.assertEqual(x4.crucial, data['crucial'])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
||||||
|
def test_create_constituenta_after(self):
|
||||||
|
self.set_params(item=self.owned_id)
|
||||||
|
|
||||||
|
data = {'alias': 'X4', 'cst_type': CstType.BASE, 'insert_after': self.invalid_cst}
|
||||||
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
data['insert_after'] = self.unowned_cst.pk
|
||||||
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'alias': 'X4',
|
||||||
|
'cst_type': CstType.BASE,
|
||||||
|
'insert_after': self.x2.pk,
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data)
|
||||||
|
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
||||||
|
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
|
||||||
|
self.x3.refresh_from_db()
|
||||||
|
self.assertEqual(x4.order, 2)
|
||||||
|
self.assertEqual(self.x3.order, 3)
|
||||||
|
|
@ -36,11 +36,11 @@ class TestRSFormViewset(EndpointTester):
|
||||||
'access_policy': AccessPolicy.PROTECTED,
|
'access_policy': AccessPolicy.PROTECTED,
|
||||||
'visible': False
|
'visible': False
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||||
data['file'] = file
|
data['file'] = file
|
||||||
response = self.client.post(self.endpoint, data=data, format='multipart')
|
response = self.client.post(self.endpoint, data, format='multipart')
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
|
@ -117,21 +117,21 @@ class TestRSFormViewset(EndpointTester):
|
||||||
def test_check_expression(self):
|
def test_check_expression(self):
|
||||||
self.owned.insert_last('X1')
|
self.owned.insert_last('X1')
|
||||||
data = {'expression': 'X1=X1'}
|
data = {'expression': 'X1=X1'}
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(response.data['parseResult'], True)
|
self.assertEqual(response.data['parseResult'], True)
|
||||||
self.assertEqual(response.data['syntax'], 'math')
|
self.assertEqual(response.data['syntax'], 'math')
|
||||||
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
|
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
|
||||||
self.assertEqual(response.data['typification'], 'LOGIC')
|
self.assertEqual(response.data['typification'], 'LOGIC')
|
||||||
self.assertEqual(response.data['valueClass'], 'value')
|
self.assertEqual(response.data['valueClass'], 'value')
|
||||||
|
|
||||||
self.executeOK(data=data, item=self.unowned_id)
|
self.executeOK(data, item=self.unowned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
|
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
|
||||||
def test_check_constituenta(self):
|
def test_check_constituenta(self):
|
||||||
self.owned.insert_last('X1')
|
self.owned.insert_last('X1')
|
||||||
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
|
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(response.data['parseResult'], True)
|
self.assertEqual(response.data['parseResult'], True)
|
||||||
self.assertEqual(response.data['syntax'], 'math')
|
self.assertEqual(response.data['syntax'], 'math')
|
||||||
self.assertEqual(response.data['astText'], '[:==[A111][=[X1][X1]]]')
|
self.assertEqual(response.data['astText'], '[:==[A111][=[X1][X1]]]')
|
||||||
|
|
@ -143,7 +143,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
def test_check_constituenta_error(self):
|
def test_check_constituenta_error(self):
|
||||||
self.owned.insert_last('X1')
|
self.owned.insert_last('X1')
|
||||||
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
|
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(response.data['parseResult'], False)
|
self.assertEqual(response.data['parseResult'], False)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {'text': '@{1|редкий} @{X1|plur,datv}'}
|
data = {'text': '@{1|редкий} @{X1|plur,datv}'}
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
|
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
|
||||||
self.assertEqual(response.data['output'], 'редким синим слонам')
|
self.assertEqual(response.data['output'], 'редким синим слонам')
|
||||||
self.assertEqual(len(response.data['refs']), 2)
|
self.assertEqual(len(response.data['refs']), 2)
|
||||||
|
|
@ -182,7 +182,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||||
data = {'file': file}
|
data = {'file': file}
|
||||||
response = self.client.post(self.endpoint, data=data, format='multipart')
|
response = self.client.post(self.endpoint, data, format='multipart')
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
self.assertTrue(response.data['title'] != '')
|
self.assertTrue(response.data['title'] != '')
|
||||||
|
|
@ -200,57 +200,10 @@ class TestRSFormViewset(EndpointTester):
|
||||||
self.assertIn('document.json', zipped_file.namelist())
|
self.assertIn('document.json', zipped_file.namelist())
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
|
||||||
def test_create_constituenta(self):
|
|
||||||
data = {'alias': 'X3', 'cst_type': CstType.BASE}
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
|
||||||
|
|
||||||
data = {'alias': 'X3'}
|
|
||||||
self.owned.insert_last('X1')
|
|
||||||
x2 = self.owned.insert_last('X2')
|
|
||||||
self.executeBadData(item=self.owned_id)
|
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
|
||||||
|
|
||||||
data['cst_type'] = 'invalid'
|
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
|
||||||
|
|
||||||
data['cst_type'] = CstType.BASE
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
|
||||||
self.assertEqual(response.data['new_cst']['alias'], 'X3')
|
|
||||||
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
|
|
||||||
self.assertEqual(x3.order, 2)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'alias': 'X4',
|
|
||||||
'cst_type': CstType.BASE,
|
|
||||||
'insert_after': x2.pk,
|
|
||||||
'term_raw': 'test',
|
|
||||||
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
|
|
||||||
'definition_formal': 'invalid',
|
|
||||||
'crucial': True
|
|
||||||
}
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
|
||||||
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
|
||||||
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
|
|
||||||
self.assertEqual(x4.order, 2)
|
|
||||||
self.assertEqual(x4.term_raw, data['term_raw'])
|
|
||||||
self.assertEqual(x4.term_forms, data['term_forms'])
|
|
||||||
self.assertEqual(x4.definition_formal, data['definition_formal'])
|
|
||||||
self.assertEqual(x4.crucial, data['crucial'])
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'alias': 'X5',
|
|
||||||
'cst_type': CstType.BASE,
|
|
||||||
'insert_after': None,
|
|
||||||
'term_raw': 'test5'
|
|
||||||
}
|
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
|
||||||
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
|
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
|
||||||
def test_substitute_multiple(self):
|
def test_substitute_multiple(self):
|
||||||
self.set_params(item=self.owned_id)
|
self.set_params(item=self.owned_id)
|
||||||
|
|
||||||
x1 = self.owned.insert_last('X1')
|
x1 = self.owned.insert_last('X1')
|
||||||
x2 = self.owned.insert_last('X2')
|
x2 = self.owned.insert_last('X2')
|
||||||
d1 = self.owned.insert_last('D1')
|
d1 = self.owned.insert_last('D1')
|
||||||
|
|
@ -261,7 +214,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {'substitutions': []}
|
data = {'substitutions': []}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'substitutions': [
|
data = {'substitutions': [
|
||||||
{
|
{
|
||||||
|
|
@ -273,7 +226,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
'substitution': d2.pk
|
'substitution': d2.pk
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'substitutions': [
|
data = {'substitutions': [
|
||||||
{
|
{
|
||||||
|
|
@ -285,7 +238,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
'substitution': d2.pk
|
'substitution': d2.pk
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
response = self.executeOK(data=data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
d3.refresh_from_db()
|
d3.refresh_from_db()
|
||||||
self.assertEqual(d3.definition_formal, r'D1 \ D2')
|
self.assertEqual(d3.definition_formal, r'D1 \ D2')
|
||||||
|
|
||||||
|
|
@ -300,7 +253,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
'definition_formal': '3',
|
'definition_formal': '3',
|
||||||
'definition_raw': '4'
|
'definition_raw': '4'
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
self.assertEqual(response.data['new_cst']['alias'], 'X3')
|
self.assertEqual(response.data['new_cst']['alias'], 'X3')
|
||||||
self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE)
|
self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE)
|
||||||
self.assertEqual(response.data['new_cst']['convention'], '1')
|
self.assertEqual(response.data['new_cst']['convention'], '1')
|
||||||
|
|
@ -316,13 +269,13 @@ class TestRSFormViewset(EndpointTester):
|
||||||
self.set_params(item=self.owned_id)
|
self.set_params(item=self.owned_id)
|
||||||
|
|
||||||
data = {'items': [1337]}
|
data = {'items': [1337]}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
x1 = self.owned.insert_last('X1')
|
x1 = self.owned.insert_last('X1')
|
||||||
x2 = self.owned.insert_last('X2')
|
x2 = self.owned.insert_last('X2')
|
||||||
|
|
||||||
data = {'items': [x1.pk]}
|
data = {'items': [x1.pk]}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.assertEqual(len(response.data['items']), 1)
|
self.assertEqual(len(response.data['items']), 1)
|
||||||
|
|
@ -332,7 +285,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
x3 = self.unowned.insert_last('X1')
|
x3 = self.unowned.insert_last('X1')
|
||||||
data = {'items': [x3.pk]}
|
data = {'items': [x3.pk]}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/move-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{item}/move-cst', method='patch')
|
||||||
|
|
@ -340,13 +293,13 @@ class TestRSFormViewset(EndpointTester):
|
||||||
self.set_params(item=self.owned_id)
|
self.set_params(item=self.owned_id)
|
||||||
|
|
||||||
data = {'items': [1337], 'move_to': 0}
|
data = {'items': [1337], 'move_to': 0}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
x1 = self.owned.insert_last('X1')
|
x1 = self.owned.insert_last('X1')
|
||||||
x2 = self.owned.insert_last('X2')
|
x2 = self.owned.insert_last('X2')
|
||||||
|
|
||||||
data = {'items': [x2.pk], 'move_to': 0}
|
data = {'items': [x2.pk], 'move_to': 0}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
x1.refresh_from_db()
|
x1.refresh_from_db()
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
self.assertEqual(response.data['id'], self.owned_id)
|
self.assertEqual(response.data['id'], self.owned_id)
|
||||||
|
|
@ -355,7 +308,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
x3 = self.unowned.insert_last('X1')
|
x3 = self.unowned.insert_last('X1')
|
||||||
data = {'items': [x3.pk], 'move_to': 0}
|
data = {'items': [x3.pk], 'move_to': 0}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch')
|
@decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch')
|
||||||
|
|
@ -392,7 +345,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||||
data = {'file': file, 'load_metadata': False}
|
data = {'file': file, 'load_metadata': False}
|
||||||
response = self.client.patch(self.endpoint, data=data, format='multipart')
|
response = self.client.patch(self.endpoint, data, format='multipart')
|
||||||
self.owned.model.refresh_from_db()
|
self.owned.model.refresh_from_db()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(self.owned.model.title, 'Test11')
|
self.assertEqual(self.owned.model.title, 'Test11')
|
||||||
|
|
@ -432,7 +385,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
self.executeBadData({'target': s2.pk})
|
self.executeBadData({'target': s2.pk})
|
||||||
|
|
||||||
# Testing simple structure
|
# Testing simple structure
|
||||||
response = self.executeOK(data={'target': s1.pk})
|
response = self.executeOK({'target': s1.pk})
|
||||||
result = response.data['schema']
|
result = response.data['schema']
|
||||||
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
||||||
self.assertEqual(len(items), 2)
|
self.assertEqual(len(items), 2)
|
||||||
|
|
@ -441,7 +394,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
# Testing complex structure
|
# Testing complex structure
|
||||||
s3.refresh_from_db()
|
s3.refresh_from_db()
|
||||||
response = self.executeOK(data={'target': s3.pk})
|
response = self.executeOK({'target': s3.pk})
|
||||||
result = response.data['schema']
|
result = response.data['schema']
|
||||||
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
||||||
self.assertEqual(len(items), 8)
|
self.assertEqual(len(items), 8)
|
||||||
|
|
@ -449,151 +402,15 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
# Testing function
|
# Testing function
|
||||||
f1.refresh_from_db()
|
f1.refresh_from_db()
|
||||||
response = self.executeOK(data={'target': f1.pk})
|
response = self.executeOK({'target': f1.pk})
|
||||||
result = response.data['schema']
|
result = response.data['schema']
|
||||||
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
|
||||||
self.assertEqual(len(items), 2)
|
self.assertEqual(len(items), 2)
|
||||||
self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])')
|
self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])')
|
||||||
|
|
||||||
|
|
||||||
class TestConstituentaAPI(EndpointTester):
|
|
||||||
''' Testing Constituenta view. '''
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
|
|
||||||
self.owned_id = self.owned.model.pk
|
|
||||||
self.unowned = RSForm.create(title='Test2', alias='T2')
|
|
||||||
self.unowned_id = self.unowned.model.pk
|
|
||||||
self.cst1 = Constituenta.objects.create(
|
|
||||||
alias='X1',
|
|
||||||
cst_type=CstType.BASE,
|
|
||||||
schema=self.owned.model,
|
|
||||||
order=0,
|
|
||||||
convention='Test',
|
|
||||||
term_raw='Test1',
|
|
||||||
term_resolved='Test1R',
|
|
||||||
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
|
|
||||||
self.cst2 = Constituenta.objects.create(
|
|
||||||
alias='X2',
|
|
||||||
cst_type=CstType.BASE,
|
|
||||||
schema=self.unowned.model,
|
|
||||||
order=0,
|
|
||||||
convention='Test1',
|
|
||||||
term_raw='Test2',
|
|
||||||
term_resolved='Test2R'
|
|
||||||
)
|
|
||||||
self.cst3 = Constituenta.objects.create(
|
|
||||||
alias='X3',
|
|
||||||
schema=self.owned.model,
|
|
||||||
order=1,
|
|
||||||
term_raw='Test3',
|
|
||||||
term_resolved='Test3',
|
|
||||||
definition_raw='Test1',
|
|
||||||
definition_resolved='Test2'
|
|
||||||
)
|
|
||||||
self.invalid_cst = self.cst3.pk + 1337
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
|
||||||
def test_partial_update(self):
|
|
||||||
data = {'target': self.cst1.pk, 'item_data': {'convention': 'tt'}}
|
|
||||||
self.executeForbidden(data=data, schema=self.unowned_id)
|
|
||||||
|
|
||||||
self.logout()
|
|
||||||
self.executeForbidden(data=data, schema=self.owned_id)
|
|
||||||
|
|
||||||
self.login()
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
self.cst1.refresh_from_db()
|
|
||||||
self.assertEqual(self.cst1.convention, 'tt')
|
|
||||||
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
|
||||||
def test_partial_update_rename(self):
|
|
||||||
data = {'target': self.cst1.pk, 'item_data': {'alias': self.cst3.alias}}
|
|
||||||
self.executeBadData(data=data, schema=self.owned_id)
|
|
||||||
|
|
||||||
d1 = self.owned.insert_last(
|
|
||||||
alias='D1',
|
|
||||||
term_raw='@{X1|plur}',
|
|
||||||
definition_formal='X1'
|
|
||||||
)
|
|
||||||
self.assertEqual(self.cst1.order, 0)
|
|
||||||
self.assertEqual(self.cst1.alias, 'X1')
|
|
||||||
self.assertEqual(self.cst1.cst_type, CstType.BASE)
|
|
||||||
|
|
||||||
data = {'target': self.cst1.pk, 'item_data': {'alias': 'D2', 'cst_type': CstType.TERM}}
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
d1.refresh_from_db()
|
|
||||||
self.cst1.refresh_from_db()
|
|
||||||
self.assertEqual(d1.term_resolved, '')
|
|
||||||
self.assertEqual(d1.term_raw, '@{D2|plur}')
|
|
||||||
self.assertEqual(self.cst1.order, 0)
|
|
||||||
self.assertEqual(self.cst1.alias, 'D2')
|
|
||||||
self.assertEqual(self.cst1.cst_type, CstType.TERM)
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
|
||||||
def test_update_resolved_no_refs(self):
|
|
||||||
data = {
|
|
||||||
'target': self.cst3.pk,
|
|
||||||
'item_data': {
|
|
||||||
'term_raw': 'New term',
|
|
||||||
'definition_raw': 'New def'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
self.cst3.refresh_from_db()
|
|
||||||
self.assertEqual(self.cst3.term_resolved, 'New term')
|
|
||||||
self.assertEqual(self.cst3.definition_resolved, 'New def')
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
|
||||||
def test_update_resolved_refs(self):
|
|
||||||
data = {
|
|
||||||
'target': self.cst3.pk,
|
|
||||||
'item_data': {
|
|
||||||
'term_raw': '@{X1|nomn,sing}',
|
|
||||||
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
self.cst3.refresh_from_db()
|
|
||||||
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
|
|
||||||
self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1')
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
|
||||||
def test_update_term_forms(self):
|
|
||||||
data = {
|
|
||||||
'target': self.cst3.pk,
|
|
||||||
'item_data': {
|
|
||||||
'definition_raw': '@{X3|sing,datv}',
|
|
||||||
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
self.cst3.refresh_from_db()
|
|
||||||
self.assertEqual(self.cst3.definition_resolved, 'form1')
|
|
||||||
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms'])
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/update-crucial', method='patch')
|
|
||||||
def test_update_crucial(self):
|
|
||||||
data = {'target': [self.cst1.pk], 'value': True}
|
|
||||||
self.executeForbidden(data=data, schema=self.unowned_id)
|
|
||||||
|
|
||||||
self.logout()
|
|
||||||
self.executeForbidden(data=data, schema=self.owned_id)
|
|
||||||
|
|
||||||
self.login()
|
|
||||||
self.executeOK(data=data, schema=self.owned_id)
|
|
||||||
self.cst1.refresh_from_db()
|
|
||||||
self.assertEqual(self.cst1.crucial, True)
|
|
||||||
|
|
||||||
|
|
||||||
class TestInlineSynthesis(EndpointTester):
|
class TestInlineSynthesis(EndpointTester):
|
||||||
''' Testing Operations endpoints. '''
|
''' Testing Inline synthesis. '''
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/inline-synthesis', method='patch')
|
@decl_endpoint('/api/rsforms/inline-synthesis', method='patch')
|
||||||
|
|
@ -612,20 +429,20 @@ class TestInlineSynthesis(EndpointTester):
|
||||||
'items': [],
|
'items': [],
|
||||||
'substitutions': []
|
'substitutions': []
|
||||||
}
|
}
|
||||||
self.executeForbidden(data=data)
|
self.executeForbidden(data)
|
||||||
|
|
||||||
data['receiver'] = invalid_id
|
data['receiver'] = invalid_id
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['receiver'] = self.schema1.model.pk
|
data['receiver'] = self.schema1.model.pk
|
||||||
data['source'] = invalid_id
|
data['source'] = invalid_id
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data['source'] = self.schema1.model.pk
|
data['source'] = self.schema1.model.pk
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
|
|
||||||
data['items'] = [invalid_id]
|
data['items'] = [invalid_id]
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
|
||||||
def test_inline_synthesis(self):
|
def test_inline_synthesis(self):
|
||||||
|
|
@ -654,7 +471,7 @@ class TestInlineSynthesis(EndpointTester):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
result = {item['alias']: item for item in response.data['items']}
|
result = {item['alias']: item for item in response.data['items']}
|
||||||
self.assertEqual(len(result), 6)
|
self.assertEqual(len(result), 6)
|
||||||
self.assertEqual(result['S1']['definition_formal'], 'X2')
|
self.assertEqual(result['S1']['definition_formal'], 'X2')
|
||||||
|
|
|
||||||
|
|
@ -8,30 +8,30 @@ class TestRSLanguageViews(EndpointTester):
|
||||||
@decl_endpoint('/api/rslang/to-ascii', method='post')
|
@decl_endpoint('/api/rslang/to-ascii', method='post')
|
||||||
def test_convert_to_ascii(self):
|
def test_convert_to_ascii(self):
|
||||||
data = {'data': '1=1'}
|
data = {'data': '1=1'}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'expression': '1=1'}
|
data = {'expression': '1=1'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertEqual(response.data['result'], r'1 \eq 1')
|
self.assertEqual(response.data['result'], r'1 \eq 1')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rslang/to-math', method='post')
|
@decl_endpoint('/api/rslang/to-math', method='post')
|
||||||
def test_convert_to_math(self):
|
def test_convert_to_math(self):
|
||||||
data = {'data': r'1 \eq 1'}
|
data = {'data': r'1 \eq 1'}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'expression': r'1 \eq 1'}
|
data = {'expression': r'1 \eq 1'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertEqual(response.data['result'], r'1=1')
|
self.assertEqual(response.data['result'], r'1=1')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rslang/parse-expression', method='post')
|
@decl_endpoint('/api/rslang/parse-expression', method='post')
|
||||||
def test_parse_expression(self):
|
def test_parse_expression(self):
|
||||||
data = {'data': r'1=1'}
|
data = {'data': r'1=1'}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'expression': r'1=1'}
|
data = {'expression': r'1=1'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertEqual(response.data['parseResult'], True)
|
self.assertEqual(response.data['parseResult'], True)
|
||||||
self.assertEqual(response.data['syntax'], 'math')
|
self.assertEqual(response.data['syntax'], 'math')
|
||||||
self.assertEqual(response.data['astText'], '[=[1][1]]')
|
self.assertEqual(response.data['astText'], '[=[1][1]]')
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
'restore_order',
|
'restore_order',
|
||||||
'reset_aliases',
|
'reset_aliases',
|
||||||
'produce_structure',
|
'produce_structure',
|
||||||
|
'add_attribution',
|
||||||
|
'delete_attribution',
|
||||||
|
'clear_attributions'
|
||||||
]:
|
]:
|
||||||
permission_list = [permissions.ItemEditor]
|
permission_list = [permissions.ItemEditor]
|
||||||
elif self.action in [
|
elif self.action in [
|
||||||
|
|
@ -79,7 +82,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
def create_cst(self, request: Request, pk) -> HttpResponse:
|
def create_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create Constituenta. '''
|
''' Create Constituenta. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CstCreateSerializer(data=request.data)
|
serializer = s.CstCreateSerializer(data=request.data, context={'schema': item})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
if 'insert_after' not in data:
|
if 'insert_after' not in data:
|
||||||
|
|
@ -232,10 +235,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
def substitute(self, request: Request, pk) -> HttpResponse:
|
def substitute(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Substitute occurrences of constituenta with another one. '''
|
''' Substitute occurrences of constituenta with another one. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CstSubstituteSerializer(
|
serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': item})
|
||||||
data=request.data,
|
|
||||||
context={'schema': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||||
|
|
||||||
|
|
@ -269,10 +269,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete multiple Constituents. '''
|
''' Endpoint: Delete multiple Constituents. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CstListSerializer(
|
serializer = s.CstListSerializer(data=request.data, context={'schema': item})
|
||||||
data=request.data,
|
|
||||||
context={'schema': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
cst_list: list[m.Constituenta] = serializer.validated_data['items']
|
cst_list: list[m.Constituenta] = serializer.validated_data['items']
|
||||||
|
|
||||||
|
|
@ -287,6 +284,106 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
data=s.RSFormParseSerializer(item).data
|
data=s.RSFormParseSerializer(item).data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='create Attribution',
|
||||||
|
tags=['Constituenta'],
|
||||||
|
request=s.AttributionCreateSerializer,
|
||||||
|
responses={
|
||||||
|
c.HTTP_201_CREATED: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['post'], url_path='create-attribution')
|
||||||
|
def create_attribution(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Create Attribution. '''
|
||||||
|
item = self._get_item()
|
||||||
|
serializer = s.AttributionCreateSerializer(data=request.data, context={'schema': item})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
container = serializer.validated_data['container']
|
||||||
|
attribute = serializer.validated_data['attribute']
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
new_association = m.Attribution.objects.create(
|
||||||
|
container=container,
|
||||||
|
attribute=attribute
|
||||||
|
)
|
||||||
|
PropagationFacade.after_create_attribution(item.pk, [new_association])
|
||||||
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_201_CREATED,
|
||||||
|
data=s.RSFormParseSerializer(item).data
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='delete Association',
|
||||||
|
tags=['RSForm'],
|
||||||
|
request=s.AttributionDataSerializer,
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['patch'], url_path='delete-attribution')
|
||||||
|
def delete_attribution(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Endpoint: Delete Attribution. '''
|
||||||
|
item = self._get_item()
|
||||||
|
serializer = s.AttributionDataSerializer(data=request.data, context={'schema': item})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
target = list(m.Attribution.objects.filter(
|
||||||
|
container=serializer.validated_data['container'],
|
||||||
|
attribute=serializer.validated_data['attribute']
|
||||||
|
))
|
||||||
|
if not target:
|
||||||
|
raise ValidationError({
|
||||||
|
'container': msg.invalidAssociation()
|
||||||
|
})
|
||||||
|
|
||||||
|
PropagationFacade.before_delete_attribution(item.pk, target)
|
||||||
|
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||||
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=s.RSFormParseSerializer(item).data
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='delete all Attributions for target constituenta',
|
||||||
|
tags=['RSForm'],
|
||||||
|
request=s.CstTargetSerializer,
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['patch'], url_path='clear-attributions')
|
||||||
|
def clear_attributions(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Endpoint: Delete Associations for target Constituenta. '''
|
||||||
|
item = self._get_item()
|
||||||
|
serializer = s.CstTargetSerializer(data=request.data, context={'schema': item})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
target = list(m.Attribution.objects.filter(container=serializer.validated_data['target']))
|
||||||
|
if target:
|
||||||
|
PropagationFacade.before_delete_attribution(item.pk, target)
|
||||||
|
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||||
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=s.RSFormParseSerializer(item).data
|
||||||
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='move constituenta',
|
summary='move constituenta',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
|
|
@ -302,10 +399,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
def move_cst(self, request: Request, pk) -> HttpResponse:
|
def move_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Move multiple Constituents. '''
|
''' Endpoint: Move multiple Constituents. '''
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
serializer = s.CstMoveSerializer(
|
serializer = s.CstMoveSerializer(data=request.data, context={'schema': item})
|
||||||
data=request.data,
|
|
||||||
context={'schema': item}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
@ -397,10 +491,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
data['id'] = item.pk
|
data['id'] = item.pk
|
||||||
|
|
||||||
serializer = s.RSFormTRSSerializer(
|
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
|
||||||
data=data,
|
|
||||||
context={'load_meta': load_metadata}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
result: m.RSForm = serializer.save()
|
result: m.RSForm = serializer.save()
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -558,10 +649,7 @@ class TrsImportView(views.APIView):
|
||||||
)
|
)
|
||||||
owner = cast(User, self.request.user)
|
owner = cast(User, self.request.user)
|
||||||
_prepare_rsform_data(data, request, owner)
|
_prepare_rsform_data(data, request, owner)
|
||||||
serializer = s.RSFormTRSSerializer(
|
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||||
data=data,
|
|
||||||
context={'load_meta': True}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema: m.RSForm = serializer.save()
|
schema: m.RSForm = serializer.save()
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -640,15 +728,12 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
|
||||||
@api_view(['PATCH'])
|
@api_view(['PATCH'])
|
||||||
def inline_synthesis(request: Request) -> HttpResponse:
|
def inline_synthesis(request: Request) -> HttpResponse:
|
||||||
''' Endpoint: Inline synthesis. '''
|
''' Endpoint: Inline synthesis. '''
|
||||||
serializer = s.InlineSynthesisSerializer(
|
serializer = s.InlineSynthesisSerializer(data=request.data, context={'user': request.user})
|
||||||
data=request.data,
|
|
||||||
context={'user': request.user}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
receiver = m.RSFormCached(serializer.validated_data['receiver'])
|
receiver = m.RSFormCached(serializer.validated_data['receiver'])
|
||||||
items = cast(list[m.Constituenta], serializer.validated_data['items'])
|
items = cast(list[m.Constituenta], serializer.validated_data['items'])
|
||||||
if len(items) == 0:
|
if not items:
|
||||||
source = cast(LibraryItem, serializer.validated_data['source'])
|
source = cast(LibraryItem, serializer.validated_data['source'])
|
||||||
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))
|
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,15 @@ class TestUserAPIViews(EndpointTester):
|
||||||
def test_login(self):
|
def test_login(self):
|
||||||
self.logout()
|
self.logout()
|
||||||
data = {'username': self.user.username, 'password': 'invalid'}
|
data = {'username': self.user.username, 'password': 'invalid'}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'username': self.user.username, 'password': 'password'}
|
data = {'username': self.user.username, 'password': 'password'}
|
||||||
self.executeAccepted(data=data)
|
self.executeAccepted(data)
|
||||||
self.executeAccepted(data=data)
|
self.executeAccepted(data)
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
data = {'username': self.user.email, 'password': 'password'}
|
data = {'username': self.user.email, 'password': 'password'}
|
||||||
self.executeAccepted(data=data)
|
self.executeAccepted(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/users/api/logout', method='post')
|
@decl_endpoint('/users/api/logout', method='post')
|
||||||
|
|
@ -82,7 +82,7 @@ class TestUserUserProfileAPIView(EndpointTester):
|
||||||
'first_name': 'firstName',
|
'first_name': 'firstName',
|
||||||
'last_name': 'lastName',
|
'last_name': 'lastName',
|
||||||
}
|
}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
self.assertEqual(response.data['email'], '123@mail.ru')
|
self.assertEqual(response.data['email'], '123@mail.ru')
|
||||||
self.assertEqual(self.user.email, '123@mail.ru')
|
self.assertEqual(self.user.email, '123@mail.ru')
|
||||||
|
|
@ -96,13 +96,13 @@ class TestUserUserProfileAPIView(EndpointTester):
|
||||||
'first_name': 'new',
|
'first_name': 'new',
|
||||||
'last_name': 'new2',
|
'last_name': 'new2',
|
||||||
}
|
}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data)
|
||||||
|
|
||||||
data = {'email': self.user2.email}
|
data = {'email': self.user2.email}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {'username': 'new_username'}
|
data = {'username': 'new_username'}
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data)
|
||||||
self.assertNotEqual(response.data['username'], data['username'])
|
self.assertNotEqual(response.data['username'], data['username'])
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|
@ -115,14 +115,14 @@ class TestUserUserProfileAPIView(EndpointTester):
|
||||||
'old_password': 'invalid',
|
'old_password': 'invalid',
|
||||||
'new_password': 'password2'
|
'new_password': 'password2'
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'old_password': 'password',
|
'old_password': 'password',
|
||||||
'new_password': 'password2'
|
'new_password': 'password2'
|
||||||
}
|
}
|
||||||
oldHash = self.user.password
|
oldHash = self.user.password
|
||||||
response = self.executeNoContent(data=data)
|
response = self.executeNoContent(data)
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
self.assertNotEqual(self.user.password, oldHash)
|
self.assertNotEqual(self.user.password, oldHash)
|
||||||
self.assertTrue(self.client.login(username=self.user.username, password='password2'))
|
self.assertTrue(self.client.login(username=self.user.username, password='password2'))
|
||||||
|
|
@ -155,7 +155,7 @@ class TestSignupAPIView(EndpointTester):
|
||||||
'first_name': 'firstName',
|
'first_name': 'firstName',
|
||||||
'last_name': 'lastName'
|
'last_name': 'lastName'
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'username': 'NewUser',
|
'username': 'NewUser',
|
||||||
|
|
@ -165,7 +165,7 @@ class TestSignupAPIView(EndpointTester):
|
||||||
'first_name': 'firstName',
|
'first_name': 'firstName',
|
||||||
'last_name': 'lastName'
|
'last_name': 'lastName'
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data)
|
response = self.executeCreated(data)
|
||||||
self.assertTrue('id' in response.data)
|
self.assertTrue('id' in response.data)
|
||||||
self.assertEqual(response.data['username'], data['username'])
|
self.assertEqual(response.data['username'], data['username'])
|
||||||
self.assertEqual(response.data['email'], data['email'])
|
self.assertEqual(response.data['email'], data['email'])
|
||||||
|
|
@ -180,7 +180,7 @@ class TestSignupAPIView(EndpointTester):
|
||||||
'first_name': 'firstName',
|
'first_name': 'firstName',
|
||||||
'last_name': 'lastName'
|
'last_name': 'lastName'
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'username': 'NewUser2',
|
'username': 'NewUser2',
|
||||||
|
|
@ -190,4 +190,4 @@ class TestSignupAPIView(EndpointTester):
|
||||||
'first_name': 'firstName',
|
'first_name': 'firstName',
|
||||||
'last_name': 'lastName'
|
'last_name': 'lastName'
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ def constituentsInvalid(constituents: list[int]):
|
||||||
return f'некорректные конституенты для схемы: {constituents}'
|
return f'некорректные конституенты для схемы: {constituents}'
|
||||||
|
|
||||||
|
|
||||||
|
def associationSelf():
|
||||||
|
return 'Рефлексивная ассоциация не допускается'
|
||||||
|
|
||||||
|
|
||||||
|
def associationAlreadyExists():
|
||||||
|
return 'Отношение уже существует'
|
||||||
|
|
||||||
|
|
||||||
def constituentaNotInRSform(title: str):
|
def constituentaNotInRSform(title: str):
|
||||||
return f'Конституента не принадлежит схеме: {title}'
|
return f'Конституента не принадлежит схеме: {title}'
|
||||||
|
|
||||||
|
|
@ -22,6 +30,10 @@ def operationNotInOSS():
|
||||||
return 'Операция не принадлежит ОСС'
|
return 'Операция не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
||||||
|
def duplicateSchemasInArguments():
|
||||||
|
return 'Аргументы не должны содержать повторяющиеся КС'
|
||||||
|
|
||||||
|
|
||||||
def blockNotInOSS():
|
def blockNotInOSS():
|
||||||
return 'Блок не принадлежит ОСС'
|
return 'Блок не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
@ -86,12 +98,12 @@ def operationInputAlreadyConnected():
|
||||||
return 'Схема уже подключена к другой операции'
|
return 'Схема уже подключена к другой операции'
|
||||||
|
|
||||||
|
|
||||||
def referenceTypeNotAllowed():
|
def replicaNotAllowed():
|
||||||
return 'Ссылки не поддерживаются'
|
return 'Реплики не поддерживаются'
|
||||||
|
|
||||||
|
|
||||||
def referenceTypeRequired():
|
def replicaRequired():
|
||||||
return 'Операция должна быть ссылкой'
|
return 'Операция должна быть репликацией'
|
||||||
|
|
||||||
|
|
||||||
def operationNotSynthesis(title: str):
|
def operationNotSynthesis(title: str):
|
||||||
|
|
@ -138,6 +150,10 @@ def typificationInvalidStr():
|
||||||
return 'Invalid typification string'
|
return 'Invalid typification string'
|
||||||
|
|
||||||
|
|
||||||
|
def invalidAssociation():
|
||||||
|
return f'Ассоциация не найдена'
|
||||||
|
|
||||||
|
|
||||||
def exteorFileVersionNotSupported():
|
def exteorFileVersionNotSupported():
|
||||||
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@ def write_zipped_json(json_data: dict, json_filename: str) -> bytes:
|
||||||
content = BytesIO()
|
content = BytesIO()
|
||||||
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
||||||
with ZipFile(content, 'w') as archive:
|
with ZipFile(content, 'w') as archive:
|
||||||
archive.writestr(json_filename, data=data)
|
archive.writestr(json_filename, data)
|
||||||
return content.getvalue()
|
return content.getvalue()
|
||||||
|
|
|
||||||
2703
rsconcept/frontend/package-lock.json
generated
2703
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -10,77 +10,77 @@
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
|
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 1 --fix",
|
||||||
"preview": "vite preview --port 3000"
|
"preview": "vite preview --port 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.5",
|
"@dagrejs/dagre": "^1.1.5",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.83.0",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-themes": "^4.24.1",
|
"@uiw/codemirror-themes": "^4.25.2",
|
||||||
"@uiw/react-codemirror": "^4.24.1",
|
"@uiw/react-codemirror": "^4.25.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.12.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lucide-react": "^0.533.0",
|
"lucide-react": "^0.545.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.61.1",
|
"react-hook-form": "^7.65.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.11",
|
"react-intl": "^7.1.14",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.9.4",
|
||||||
"react-scan": "^0.4.3",
|
"react-scan": "^0.4.3",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.29.1",
|
"react-tooltip": "^5.30.0",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.7",
|
||||||
"use-debounce": "^10.0.5",
|
"use-debounce": "^10.0.6",
|
||||||
"zod": "^4.0.13",
|
"zod": "^4.1.12",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@playwright/test": "^1.54.1",
|
"@playwright/test": "^1.56.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.7.2",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-playwright": "^2.2.1",
|
"eslint-plugin-playwright": "^2.2.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.0.5",
|
"jest": "^30.2.0",
|
||||||
"stylelint": "^16.23.0",
|
"stylelint": "^16.25.0",
|
||||||
"stylelint-config-recommended": "^16.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.38.0",
|
"typescript-eslint": "^8.46.0",
|
||||||
"vite": "^7.0.6"
|
"vite": "^7.1.9"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 131 KiB |
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useNavigate, useRouteError } from 'react-router';
|
import { useNavigate, useRouteError } from 'react-router';
|
||||||
|
|
||||||
import { Button } from '@/components/control';
|
import { Button } from '@/components/control';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import React from 'react';
|
||||||
|
|
||||||
import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
const DlgShowVideo = React.lazy(() =>
|
||||||
|
import('@/features/help/dialogs/dlg-show-video').then(module => ({ default: module.DlgShowVideo }))
|
||||||
|
);
|
||||||
const DlgChangeInputSchema = React.lazy(() =>
|
const DlgChangeInputSchema = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
|
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
|
||||||
);
|
);
|
||||||
|
|
@ -46,8 +49,8 @@ const DlgDeleteOperation = React.lazy(() =>
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgDeleteReference = React.lazy(() =>
|
const DlgDeleteReference = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
|
import('@/features/oss/dialogs/dlg-delete-replica').then(module => ({
|
||||||
default: module.DlgDeleteReference
|
default: module.DlgDeleteReplica
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditEditors = React.lazy(() =>
|
const DlgEditEditors = React.lazy(() =>
|
||||||
|
|
@ -161,6 +164,8 @@ export const GlobalDialogs = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
switch (active) {
|
switch (active) {
|
||||||
|
case DialogType.SHOW_VIDEO:
|
||||||
|
return <DlgShowVideo />;
|
||||||
case DialogType.CONSTITUENTA_TEMPLATE:
|
case DialogType.CONSTITUENTA_TEMPLATE:
|
||||||
return <DlgCstTemplate />;
|
return <DlgCstTemplate />;
|
||||||
case DialogType.CREATE_CONSTITUENTA:
|
case DialogType.CREATE_CONSTITUENTA:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useNavigation } from 'react-router';
|
import { useNavigation } from 'react-router';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
||||||
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Tooltip } from '@/components/container';
|
import { Tooltip } from '@/components/container';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useAuth } from '@/features/auth/backend/use-auth';
|
import { useAuth } from '@/features/auth/backend/use-auth';
|
||||||
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||||
|
|
@ -12,34 +14,40 @@ import { useConceptNavigation } from './navigation-context';
|
||||||
|
|
||||||
export function MenuAI() {
|
export function MenuAI() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
|
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
|
||||||
|
|
||||||
function navigateTemplates(event: React.MouseEvent<Element>) {
|
function navigateTemplates(event: React.MouseEvent<Element>) {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
|
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreatePrompt(event: React.MouseEvent<Element>) {
|
function handleCreatePrompt(event: React.MouseEvent<Element>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
menu.hide();
|
hideMenu();
|
||||||
showAIPrompt();
|
showAIPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
title='ИИ помощник' //
|
title='ИИ помощник' //
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
aria-expanded={menu.isOpen}
|
aria-expanded={isMenuOpen}
|
||||||
aria-controls={globalIDs.ai_dropdown}
|
aria-controls={globalIDs.ai_dropdown}
|
||||||
icon={<IconAssistant size='1.5rem' />}
|
icon={<IconAssistant size='1.5rem' />}
|
||||||
onClick={menu.toggle}
|
onClick={toggleMenu}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={menu.isOpen}>
|
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={isMenuOpen}>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Запрос'
|
text='Запрос'
|
||||||
title='Создать запрос'
|
title='Создать запрос'
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { useDropdown } from '@/components/dropdown';
|
import { useDropdown } from '@/components/dropdown';
|
||||||
|
|
@ -11,17 +13,23 @@ import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export function MenuUser() {
|
export function MenuUser() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
|
||||||
<Suspense fallback={<Loader circular scale={1.5} />}>
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
<UserButton
|
<UserButton
|
||||||
onLogin={() => router.push({ path: urls.login, force: true })}
|
onLogin={() => router.push({ path: urls.login, force: true })}
|
||||||
onClickUser={menu.toggle}
|
onClickUser={toggleMenu}
|
||||||
isOpen={menu.isOpen}
|
isOpen={isMenuOpen}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
<UserDropdown isOpen={isMenuOpen} hideDropdown={() => hideMenu()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { IconLogin, IconUser2 } from '@/components/icons';
|
import { IconLogin, IconUser2 } from '@/components/icons';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useLogout } from '@/features/auth/backend/use-logout';
|
import { useLogout } from '@/features/auth/backend/use-logout';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function ExportDropdown<T extends object = object>({
|
||||||
filename = 'export',
|
filename = 'export',
|
||||||
className
|
className
|
||||||
}: ExportDropdownProps<T>) {
|
}: ExportDropdownProps<T>) {
|
||||||
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||||
|
|
||||||
function handleExport(format: 'csv' | 'json') {
|
function handleExport(format: 'csv' | 'json') {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
'use no memo';
|
'use no memo';
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function SelectPagination<TData>({ id, table, paginationOptions, onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select onValueChange={handlePaginationOptionsChange} defaultValue={String(table.getState().pagination.pageSize)}>
|
<Select onValueChange={handlePaginationOptionsChange} value={String(table.getState().pagination.pageSize)}>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id={id}
|
id={id}
|
||||||
aria-label='Выбор количества строчек на странице'
|
aria-label='Выбор количества строчек на странице'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
'use no memo';
|
'use no memo';
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
'use client';
|
||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
'use no memo';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ export function Dropdown({
|
||||||
margin,
|
margin,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
aria-hidden={!isOpen}
|
|
||||||
inert={!isOpen}
|
inert={!isOpen}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,26 @@ import { useRef, useState } from 'react';
|
||||||
|
|
||||||
export function useDropdown() {
|
export function useDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
||||||
if (ref.current?.contains(event.relatedTarget as Node)) {
|
const nextTarget = event.relatedTarget as Node | null;
|
||||||
|
if (nextTarget && elementRef.current?.contains(nextTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep open when focus moves into a popover (e.g., ComboBox menu rendered via portal)
|
||||||
|
if (
|
||||||
|
nextTarget instanceof Element &&
|
||||||
|
(nextTarget.closest("[data-slot='popover-content']") || nextTarget.closest("[data-slot='popover-trigger']"))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref,
|
elementRef,
|
||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
handleBlur,
|
handleBlur,
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ export { BiX as IconRemove } from 'react-icons/bi';
|
||||||
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
||||||
export { BiReset as IconReset } from 'react-icons/bi';
|
export { BiReset as IconReset } from 'react-icons/bi';
|
||||||
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
|
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
|
||||||
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
export { FiEdit as IconEdit } from 'react-icons/fi';
|
||||||
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
export { AiOutlineEdit as IconEdit2 } from 'react-icons/ai';
|
||||||
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||||
export { BiDownload as IconDownload } from 'react-icons/bi';
|
export { BiDownload as IconDownload } from 'react-icons/bi';
|
||||||
export { BiUpload as IconUpload } from 'react-icons/bi';
|
export { BiUpload as IconUpload } from 'react-icons/bi';
|
||||||
|
|
@ -39,6 +39,7 @@ export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
|
||||||
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
||||||
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
||||||
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
||||||
|
export { IoVideocamOutline as IconVideo } from 'react-icons/io5';
|
||||||
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
|
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
|
||||||
export { LuFolder as IconFolder } from 'react-icons/lu';
|
export { LuFolder as IconFolder } from 'react-icons/lu';
|
||||||
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
|
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
|
||||||
|
|
@ -87,6 +88,7 @@ export { MdOutlineSelectAll as IconConceptBlock } from 'react-icons/md';
|
||||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||||
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
||||||
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
||||||
|
export { TbHexagonLetterN as IconCstNominal } from 'react-icons/tb';
|
||||||
export { TbHexagonLetterX as IconCstBaseSet } from 'react-icons/tb';
|
export { TbHexagonLetterX as IconCstBaseSet } from 'react-icons/tb';
|
||||||
export { TbHexagonLetterC as IconCstConstSet } from 'react-icons/tb';
|
export { TbHexagonLetterC as IconCstConstSet } from 'react-icons/tb';
|
||||||
export { TbHexagonLetterS as IconCstStructured } from 'react-icons/tb';
|
export { TbHexagonLetterS as IconCstStructured } from 'react-icons/tb';
|
||||||
|
|
@ -125,7 +127,7 @@ export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
||||||
export { RiShieldLine as IconProtected } from 'react-icons/ri';
|
export { RiShieldLine as IconProtected } from 'react-icons/ri';
|
||||||
export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri';
|
export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri';
|
||||||
export { BiBug as IconStatusError } from 'react-icons/bi';
|
export { BiBug as IconStatusError } from 'react-icons/bi';
|
||||||
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
|
export { LuThumbsUp as IconStatusOK } from 'react-icons/lu';
|
||||||
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
||||||
export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
|
export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
|
||||||
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
|
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
|
||||||
|
|
@ -160,7 +162,6 @@ export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
||||||
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
|
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
|
||||||
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
||||||
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
||||||
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
|
|
||||||
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
||||||
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
|
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
|
||||||
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -9,20 +11,32 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||||
import { cn } from '../utils';
|
import { cn } from '../utils';
|
||||||
|
|
||||||
interface ComboMultiProps<Option> extends Styling {
|
interface ComboMultiPropsBase<Option> extends Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
items?: Option[];
|
items?: Option[];
|
||||||
value: Option[];
|
value: Option[];
|
||||||
onChange: (newValue: Option[]) => void;
|
|
||||||
|
|
||||||
idFunc: (item: Option) => string;
|
idFunc: (item: Option) => string;
|
||||||
labelValueFunc: (item: Option) => string;
|
labelValueFunc: (item: Option) => string;
|
||||||
labelOptionFunc: (item: Option) => string;
|
labelOptionFunc: (item: Option) => string;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noSearch?: boolean;
|
noSearch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ComboMultiPropsFull<Option> extends ComboMultiPropsBase<Option> {
|
||||||
|
onChange: (newValue: Option[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComboMultiPropsSplit<Option> extends ComboMultiPropsBase<Option> {
|
||||||
|
onClear: () => void;
|
||||||
|
onAdd: (item: Option) => void;
|
||||||
|
onRemove: (item: Option) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComboMultiProps<Option> = ComboMultiPropsFull<Option> | ComboMultiPropsSplit<Option>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a combo-box component with multiple selection.
|
* Displays a combo-box component with multiple selection.
|
||||||
*/
|
*/
|
||||||
|
|
@ -30,14 +44,15 @@ export function ComboMulti<Option>({
|
||||||
id,
|
id,
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
onChange,
|
|
||||||
labelValueFunc,
|
labelValueFunc,
|
||||||
labelOptionFunc,
|
labelOptionFunc,
|
||||||
idFunc,
|
idFunc,
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
noSearch
|
disabled,
|
||||||
|
noSearch,
|
||||||
|
...restProps
|
||||||
}: ComboMultiProps<Option>) {
|
}: ComboMultiProps<Option>) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
||||||
|
|
@ -54,19 +69,34 @@ export function ComboMulti<Option>({
|
||||||
if (value.includes(newValue)) {
|
if (value.includes(newValue)) {
|
||||||
handleRemoveValue(newValue);
|
handleRemoveValue(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange([...value, newValue]);
|
if ('onAdd' in restProps && typeof restProps.onAdd === 'function') {
|
||||||
|
restProps.onAdd(newValue);
|
||||||
|
} else {
|
||||||
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange([...value, newValue]);
|
||||||
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveValue(delValue: Option) {
|
function handleRemoveValue(delValue: Option) {
|
||||||
onChange(value.filter(v => v !== delValue));
|
if ('onRemove' in restProps && typeof restProps.onRemove === 'function') {
|
||||||
|
restProps.onRemove(delValue);
|
||||||
|
} else {
|
||||||
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange(value.filter(v => v !== delValue));
|
||||||
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClear(event: React.MouseEvent<SVGElement>) {
|
function handleClear(event: React.MouseEvent<SVGElement>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onChange([]);
|
if ('onClear' in restProps && typeof restProps.onClear === 'function') {
|
||||||
|
restProps.onClear();
|
||||||
|
} else {
|
||||||
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange([]);
|
||||||
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +111,7 @@ export function ComboMulti<Option>({
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative h-9',
|
'relative h-9',
|
||||||
'flex gap-2 px-3 py-2 items-center justify-between',
|
'flex gap-2 px-3 py-2 items-center justify-between',
|
||||||
'bg-input disabled:opacity-50',
|
'bg-input disabled:bg-transparent',
|
||||||
'cursor-pointer disabled:cursor-auto',
|
'cursor-pointer disabled:cursor-auto',
|
||||||
'whitespace-nowrap',
|
'whitespace-nowrap',
|
||||||
'focus-outline border',
|
'focus-outline border',
|
||||||
|
|
@ -91,32 +121,39 @@ export function ComboMulti<Option>({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className='flex flex-wrap gap-1 items-center'>
|
<div className='flex flex-wrap gap-2 items-center'>
|
||||||
{value.length === 0 ? <div className='text-muted-foreground'>{placeholder}</div> : null}
|
{value.length === 0 ? <div className='text-muted-foreground'>{placeholder}</div> : null}
|
||||||
{value.map(item => (
|
{value.map(item => (
|
||||||
<div key={idFunc(item)} className='flex px-1 items-center border rounded-lg bg-accent text-sm'>
|
<div key={idFunc(item)} className='flex px-1 items-center border rounded-lg bg-accent text-sm'>
|
||||||
{labelValueFunc(item)}
|
{labelValueFunc(item)}
|
||||||
<IconRemove
|
{!disabled ? (
|
||||||
tabIndex={-1}
|
<IconRemove
|
||||||
size='1rem'
|
tabIndex={-1}
|
||||||
className='cc-remove cc-hover-pulse'
|
size='1rem'
|
||||||
onClick={event => {
|
className='cc-remove cc-hover-pulse'
|
||||||
event.stopPropagation();
|
onClick={
|
||||||
handleRemoveValue(item);
|
disabled
|
||||||
}}
|
? undefined
|
||||||
/>
|
: event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleRemoveValue(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChevronDownIcon className={cn('text-muted-foreground', !!value && 'opacity-0')} />
|
<ChevronDownIcon className={cn('text-muted-foreground', !!value && 'opacity-0')} />
|
||||||
{!!value ? (
|
{!!value && !disabled ? (
|
||||||
<IconRemove
|
<IconRemove
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
size='1rem'
|
size='1rem'
|
||||||
className='cc-remove absolute pointer-events-auto right-3 cc-hover-pulse hover:text-primary'
|
className='cc-remove absolute pointer-events-auto right-3 cc-hover-pulse hover:text-primary'
|
||||||
onClick={handleClear}
|
onClick={value.length === 0 ? undefined : handleClear}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -127,16 +164,19 @@ export function ComboMulti<Option>({
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>Список пуст</CommandEmpty>
|
<CommandEmpty>Список пуст</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{items?.map(item => (
|
{items
|
||||||
<CommandItem
|
?.filter(item => !value.includes(item))
|
||||||
key={idFunc(item)}
|
.map(item => (
|
||||||
value={labelOptionFunc(item)}
|
<CommandItem
|
||||||
onSelect={() => handleAddValue(item)}
|
key={idFunc(item)}
|
||||||
className={cn(value === item && 'bg-selected text-selected-foreground')}
|
value={labelOptionFunc(item)}
|
||||||
>
|
onSelect={() => handleAddValue(item)}
|
||||||
{labelOptionFunc(item)}
|
disabled={disabled}
|
||||||
</CommandItem>
|
className={cn(value === item && 'bg-selected text-selected-foreground')}
|
||||||
))}
|
>
|
||||||
|
{labelOptionFunc(item)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
|
|
||||||
42
rsconcept/frontend/src/components/view/embed-vkvideo.tsx
Normal file
42
rsconcept/frontend/src/components/view/embed-vkvideo.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
interface EmbedVKVideoProps {
|
||||||
|
/** Video ID to embed. */
|
||||||
|
videoID: string;
|
||||||
|
|
||||||
|
/** Display height in pixels. */
|
||||||
|
pxHeight: number;
|
||||||
|
|
||||||
|
/** Display width in pixels. */
|
||||||
|
pxWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embeds a YouTube video into the page using the given video ID and dimensions.
|
||||||
|
*/
|
||||||
|
export function EmbedVKVideo({ videoID, pxHeight, pxWidth }: EmbedVKVideoProps) {
|
||||||
|
if (!pxWidth) {
|
||||||
|
pxWidth = (pxHeight * 16) / 9;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='relative h-0 mt-1'
|
||||||
|
style={{
|
||||||
|
paddingBottom: `${pxHeight}px`,
|
||||||
|
paddingLeft: `${pxWidth}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
allowFullScreen
|
||||||
|
title='Встроенное видео ВКонтакте'
|
||||||
|
allow='autoplay; encrypted-media; fullscreen; picture-in-picture; screen-wake-lock;'
|
||||||
|
className='absolute top-0 left-0 border'
|
||||||
|
style={{
|
||||||
|
minHeight: `${pxHeight}px`,
|
||||||
|
minWidth: `${pxWidth}px`
|
||||||
|
}}
|
||||||
|
width={`${pxWidth}px`}
|
||||||
|
height={`${pxHeight}px`}
|
||||||
|
src={`https://vk.com/video_ext.php?${videoID}&hd=1`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { Suspense, useState } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
|
|
@ -18,7 +20,7 @@ export function DlgAIPromptDialog() {
|
||||||
return (
|
return (
|
||||||
<ModalView
|
<ModalView
|
||||||
header='Генератор запросом LLM'
|
header='Генератор запросом LLM'
|
||||||
className='w-100 sm:w-160 px-6 flex flex-col h-120'
|
className='w-100 sm:w-160 px-6 flex flex-col h-110'
|
||||||
helpTopic={HelpTopic.ASSISTANT}
|
helpTopic={HelpTopic.ASSISTANT}
|
||||||
>
|
>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconClone, IconEdit2 } from '@/components/icons';
|
import { IconClone, IconEdit } from '@/components/icons';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function MenuAIPrompt({ promptID, generatedPrompt }: MenuAIPromptProps) {
|
||||||
title='Редактировать шаблон'
|
title='Редактировать шаблон'
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconEdit2 size='1.25rem' />}
|
icon={<IconEdit size='1.25rem' />}
|
||||||
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
|
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
|
||||||
onClick={navigatePrompt}
|
onClick={navigatePrompt}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { TextArea } from '@/components/input';
|
import { TextArea } from '@/components/input';
|
||||||
|
|
||||||
import { PromptInput } from '../../components/prompt-input';
|
import { PromptInput } from '../../components/prompt-input';
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function TabPromptResult({ prompt }: TabPromptResultProps) {
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder='Текст шаблона пуст'
|
placeholder='Текст шаблона пуст'
|
||||||
disabled
|
disabled
|
||||||
className='w-full h-100'
|
className='w-full h-88'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
|
@ -39,7 +41,7 @@ export function DlgCreatePromptTemplate() {
|
||||||
mode: 'onChange'
|
mode: 'onChange'
|
||||||
});
|
});
|
||||||
const label = useWatch({ control, name: 'label' });
|
const label = useWatch({ control, name: 'label' });
|
||||||
const isValid = !!label && !templates.find(template => template.label === label);
|
const canSubmit = !!label && !templates.find(template => template.label === label);
|
||||||
|
|
||||||
function onSubmit(data: ICreatePromptTemplateDTO) {
|
function onSubmit(data: ICreatePromptTemplateDTO) {
|
||||||
void createPromptTemplate(data).then(onCreate);
|
void createPromptTemplate(data).then(onCreate);
|
||||||
|
|
@ -49,7 +51,7 @@ export function DlgCreatePromptTemplate() {
|
||||||
<ModalForm
|
<ModalForm
|
||||||
header='Создание шаблона'
|
header='Создание шаблона'
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={isValid}
|
canSubmit={canSubmit}
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
submitInvalidTooltip='Введите уникальное название шаблона'
|
submitInvalidTooltip='Введите уникальное название шаблона'
|
||||||
className='cc-column w-140 max-h-120 py-2 px-6'
|
className='cc-column w-140 max-h-120 py-2 px-6'
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { labelCstTypification } from '@/features/rsform/labels';
|
||||||
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
|
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
|
||||||
import { TypificationGraph } from '@/features/rsform/models/typification-graph';
|
import { TypificationGraph } from '@/features/rsform/models/typification-graph';
|
||||||
|
|
||||||
|
import { type Graph } from '@/models/graph';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { mockPromptVariable } from '../labels';
|
import { mockPromptVariable } from '../labels';
|
||||||
|
|
@ -38,30 +39,24 @@ export function generateSample(target: string): string {
|
||||||
|
|
||||||
/** Generates a prompt for a schema variable. */
|
/** Generates a prompt for a schema variable. */
|
||||||
export function varSchema(schema: IRSForm): string {
|
export function varSchema(schema: IRSForm): string {
|
||||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
let result = stringifySchemaIntro(schema);
|
||||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
result += '\n\nКонституенты:';
|
||||||
result += 'Конституенты:\n';
|
|
||||||
schema.items.forEach(item => {
|
schema.items.forEach(item => {
|
||||||
result += `\n${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
|
result += `\n${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
|
||||||
item.definition_formal
|
item.definition_formal
|
||||||
}" - "${item.definition_resolved}" - "${item.convention}"`;
|
}" - "${item.definition_resolved}" - "${item.convention}"`;
|
||||||
});
|
});
|
||||||
if (schema.stats.count_crucial > 0) {
|
result += `\n${stringifyCrucial(schema.items.filter(cst => cst.crucial))}`;
|
||||||
result +=
|
result += '\n\nСвязи "атрибутирован":';
|
||||||
'\nКлючевые конституенты: ' +
|
const attributionGraph = stringifyGraph(schema.attribution_graph, schema);
|
||||||
schema.items
|
result += attributionGraph ? attributionGraph : ' отсутствуют';
|
||||||
.filter(cst => cst.crucial)
|
|
||||||
.map(cst => cst.alias)
|
|
||||||
.join(', ');
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a prompt for a schema thesaurus variable. */
|
/** Generates a prompt for a schema thesaurus variable. */
|
||||||
export function varSchemaThesaurus(schema: IRSForm): string {
|
export function varSchemaThesaurus(schema: IRSForm): string {
|
||||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
let result = stringifySchemaIntro(schema);
|
||||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
result += '\n\nТермины:';
|
||||||
result += 'Термины:\n';
|
|
||||||
schema.items.forEach(item => {
|
schema.items.forEach(item => {
|
||||||
if (item.cst_type === CstType.AXIOM || item.cst_type === CstType.THEOREM) {
|
if (item.cst_type === CstType.AXIOM || item.cst_type === CstType.THEOREM) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -77,48 +72,62 @@ export function varSchemaThesaurus(schema: IRSForm): string {
|
||||||
|
|
||||||
/** Generates a prompt for a schema graph variable. */
|
/** Generates a prompt for a schema graph variable. */
|
||||||
export function varSchemaGraph(schema: IRSForm): string {
|
export function varSchemaGraph(schema: IRSForm): string {
|
||||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
let result = stringifySchemaIntro(schema);
|
||||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
result += '\n\nУзлы графа\n';
|
||||||
result += 'Узлы графа\n';
|
result += JSON.stringify(
|
||||||
result += JSON.stringify(schema.items, null, PARAMETER.indentJSON);
|
schema.items.map(cst => ({
|
||||||
result += '\n\nСвязи графа';
|
alias: cst.alias,
|
||||||
schema.graph.nodes.forEach(node => (result += `\n${node.id} -> ${node.outputs.join(', ')}`));
|
term: cst.term_resolved,
|
||||||
|
definition: cst.definition_resolved,
|
||||||
|
convention: cst.convention,
|
||||||
|
crucial: cst.crucial
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
PARAMETER.indentJSON
|
||||||
|
);
|
||||||
|
|
||||||
|
result += '\n\nСвязи "входит в определение"';
|
||||||
|
const definitionGraph = stringifyGraph(schema.graph, schema);
|
||||||
|
result += definitionGraph ? definitionGraph : ' отсутствуют';
|
||||||
|
|
||||||
|
result += '\n\nСвязи "атрибутирован"';
|
||||||
|
const attributionGraph = stringifyGraph(schema.attribution_graph, schema);
|
||||||
|
result += attributionGraph ? attributionGraph : ' отсутствуют';
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a prompt for a schema type graph variable. */
|
/** Generates a prompt for a schema type graph variable. */
|
||||||
export function varSchemaTypeGraph(schema: IRSForm): string {
|
export function varSchemaTypeGraph(schema: IRSForm): string {
|
||||||
const graph = new TypificationGraph();
|
const graph = new TypificationGraph();
|
||||||
schema.items.forEach(item => graph.addConstituenta(item.alias, item.parse.typification, item.parse.args));
|
schema.items.forEach(item => {
|
||||||
|
if (item.parse) graph.addConstituenta(item.alias, item.parse.typification, item.parse.args);
|
||||||
|
});
|
||||||
|
|
||||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
let result = stringifySchemaIntro(schema);
|
||||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
result += '\n\nСтупени\n';
|
||||||
result += 'Ступени\n';
|
|
||||||
result += JSON.stringify(graph.nodes, null, PARAMETER.indentJSON);
|
result += JSON.stringify(graph.nodes, null, PARAMETER.indentJSON);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a prompt for a OSS variable. */
|
/** Generates a prompt for a OSS variable. */
|
||||||
export function varOSS(oss: IOperationSchema): string {
|
export function varOSS(oss: IOperationSchema): string {
|
||||||
let result = `Название операционной схемы: ${oss.title}\n`;
|
let result = stringifyOSSIntro(oss);
|
||||||
result += `Сокращение: ${oss.alias}\n`;
|
result += `\n\nБлоки: ${oss.blocks.length}\n`;
|
||||||
result += `Описание: ${oss.description}\n`;
|
|
||||||
result += `Блоки: ${oss.blocks.length}\n`;
|
|
||||||
oss.hierarchy.topologicalOrder().forEach(blockID => {
|
oss.hierarchy.topologicalOrder().forEach(blockID => {
|
||||||
const block = oss.itemByNodeID.get(blockID);
|
const block = oss.itemByNodeID.get(blockID);
|
||||||
if (block?.nodeType !== NodeType.BLOCK) {
|
if (block?.nodeType !== NodeType.BLOCK) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
result += `\nБлок ${block.id}: ${block.title}\n`;
|
result += `\n\nБлок ${block.id}: ${block.title}`;
|
||||||
result += `Описание: ${block.description}\n`;
|
result += `\nОписание: ${block.description}`;
|
||||||
result += `Предок: "${block.parent}"\n`;
|
result += `\nПредок: "${block.parent ?? 'отсутствует'}"`;
|
||||||
});
|
});
|
||||||
result += `Операции: ${oss.operations.length}\n`;
|
result += `\n\nОперации: ${oss.operations.length}`;
|
||||||
oss.operations.forEach(operation => {
|
oss.operations.forEach(operation => {
|
||||||
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
|
result += `\n\nОперация ${operation.id}: ${operation.alias}`;
|
||||||
result += `Название: ${operation.title}\n`;
|
result += `\nНазвание: ${operation.title}`;
|
||||||
result += `Описание: ${operation.description}\n`;
|
result += `\nОписание: ${operation.description}`;
|
||||||
result += `Блок: ${operation.parent}`;
|
result += `\nБлок: ${operation.parent ?? 'отсутствует'}`;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -127,19 +136,19 @@ export function varOSS(oss: IOperationSchema): string {
|
||||||
export function varBlock(target: IBlock, oss: IOperationSchema): string {
|
export function varBlock(target: IBlock, oss: IOperationSchema): string {
|
||||||
const blocks = oss.blocks.filter(block => block.parent === target.id);
|
const blocks = oss.blocks.filter(block => block.parent === target.id);
|
||||||
const operations = oss.operations.filter(operation => operation.parent === target.id);
|
const operations = oss.operations.filter(operation => operation.parent === target.id);
|
||||||
let result = `Название блока: ${target.title}\n`;
|
let result = `Название блока: ${target.title}`;
|
||||||
result += `Описание: "${target.description}"\n`;
|
result += `\nОписание: "${target.description}"`;
|
||||||
result += '\nСодержание\n';
|
result += '\n\nСодержание';
|
||||||
result += `Блоки: ${blocks.length}\n`;
|
result += `\nБлоки: ${blocks.length}`;
|
||||||
blocks.forEach(block => {
|
blocks.forEach(block => {
|
||||||
result += `\nБлок ${block.id}: ${block.title}\n`;
|
result += `\n\nБлок ${block.id}: ${block.title}`;
|
||||||
result += `Описание: "${block.description}"\n`;
|
result += `\nОписание: "${block.description}"`;
|
||||||
});
|
});
|
||||||
result += `Операции: ${operations.length}\n`;
|
result += `\n\nОперации: ${operations.length}`;
|
||||||
operations.forEach(operation => {
|
operations.forEach(operation => {
|
||||||
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
|
result += `\n\nОперация ${operation.id}: ${operation.alias}`;
|
||||||
result += `Название: "${operation.title}"\n`;
|
result += `\nНазвание: "${operation.title}"`;
|
||||||
result += `Описание: "${operation.description}"`;
|
result += `\nОписание: "${operation.description}"`;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -151,9 +160,49 @@ export function varConstituenta(cst: IConstituenta): string {
|
||||||
|
|
||||||
/** Generates a prompt for a constituenta syntax tree variable. */
|
/** Generates a prompt for a constituenta syntax tree variable. */
|
||||||
export function varSyntaxTree(cst: IConstituenta): string {
|
export function varSyntaxTree(cst: IConstituenta): string {
|
||||||
let result = `Конституента: ${cst.alias}\n`;
|
let result = `Конституента: ${cst.alias}`;
|
||||||
result += `Формальное выражение: ${cst.definition_formal}\n`;
|
result += `\nФормальное выражение: ${cst.definition_formal}`;
|
||||||
result += `Дерево синтаксического разбора:\n`;
|
result += `\nДерево синтаксического разбора:\n`;
|
||||||
result += JSON.stringify(cst.parse.syntaxTree, null, PARAMETER.indentJSON);
|
result += cst.parse ? JSON.stringify(cst.parse.syntaxTree, null, PARAMETER.indentJSON) : 'не определено';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Internal functions ====
|
||||||
|
function stringifyGraph(graph: Graph<number>, schema: IRSForm): string {
|
||||||
|
let result = '';
|
||||||
|
graph.nodes.forEach(node => {
|
||||||
|
if (node.outputs.length > 0) {
|
||||||
|
result += `\n${schema.items.find(cst => cst.id === node.id)!.alias} -> ${node.outputs
|
||||||
|
.map(id => schema.items.find(cst => cst.id === id)!.alias)
|
||||||
|
.join(', ')}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifySchemaIntro(schema: IRSForm): string {
|
||||||
|
let result = `Концептуальная схема: ${schema.title}`;
|
||||||
|
result += `\nКраткое название: ${schema.alias}`;
|
||||||
|
if (schema.description) {
|
||||||
|
result += `\nОписание: "${schema.description}"`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyOSSIntro(schema: IOperationSchema): string {
|
||||||
|
let result = `Операционная схема: ${schema.title}`;
|
||||||
|
result += `\nКраткое название: ${schema.alias}`;
|
||||||
|
if (schema.description) {
|
||||||
|
result += `\nОписание: "${schema.description}"`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyCrucial(cstList: IConstituenta[]): string {
|
||||||
|
let result = 'Ключевые конституенты: ';
|
||||||
|
if (cstList.length === 0) {
|
||||||
|
return result + 'отсутствуют';
|
||||||
|
}
|
||||||
|
result += cstList.map(cst => cst.alias).join(', ');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
@ -63,24 +63,25 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
|
||||||
|
|
||||||
const prevReset = useRef(toggleReset);
|
const prevReset = useRef(toggleReset);
|
||||||
const prevTemplate = useRef(promptTemplate);
|
const prevTemplate = useRef(promptTemplate);
|
||||||
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
|
|
||||||
prevTemplate.current = promptTemplate;
|
|
||||||
prevReset.current = toggleReset;
|
|
||||||
reset({
|
|
||||||
owner: promptTemplate.owner,
|
|
||||||
label: promptTemplate.label,
|
|
||||||
description: promptTemplate.description,
|
|
||||||
text: promptTemplate.text,
|
|
||||||
is_shared: promptTemplate.is_shared
|
|
||||||
});
|
|
||||||
setSampleResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
useEffect(() => {
|
||||||
if (prevDirty.current !== isDirty) {
|
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
|
||||||
prevDirty.current = isDirty;
|
prevTemplate.current = promptTemplate;
|
||||||
|
prevReset.current = toggleReset;
|
||||||
|
reset({
|
||||||
|
owner: promptTemplate.owner,
|
||||||
|
label: promptTemplate.label,
|
||||||
|
description: promptTemplate.description,
|
||||||
|
text: promptTemplate.text,
|
||||||
|
is_shared: promptTemplate.is_shared
|
||||||
|
});
|
||||||
|
return () => setSampleResult(null);
|
||||||
|
}
|
||||||
|
}, [promptTemplate, toggleReset, reset, setSampleResult]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}
|
}, [isDirty, setIsModified]);
|
||||||
|
|
||||||
function onSubmit(data: IUpdatePromptTemplateDTO) {
|
function onSubmit(data: IUpdatePromptTemplateDTO) {
|
||||||
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useDeletePromptTemplate } from '@/features/ai/backend/use-delete-prompt-template';
|
import { useDeletePromptTemplate } from '@/features/ai/backend/use-delete-prompt-template';
|
||||||
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
|
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user