mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement subscriptions and improve UI
This commit is contained in:
parent
fba16a3d0b
commit
743ddf298e
|
@ -8,9 +8,14 @@ class ConstituentaAdmin(admin.ModelAdmin):
|
|||
''' Admin model: Constituenta. '''
|
||||
|
||||
|
||||
class Librarydmin(admin.ModelAdmin):
|
||||
class LibraryAdmin(admin.ModelAdmin):
|
||||
''' Admin model: LibraryItem. '''
|
||||
|
||||
|
||||
class SubscriptionAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Subscriptions. '''
|
||||
|
||||
|
||||
admin.site.register(models.Constituenta, ConstituentaAdmin)
|
||||
admin.site.register(models.LibraryItem, Librarydmin)
|
||||
admin.site.register(models.LibraryItem, LibraryAdmin)
|
||||
admin.site.register(models.Subscription, SubscriptionAdmin)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 4.2.4 on 2023-08-25 12:15
|
||||
# Generated by Django 4.2.4 on 2023-08-26 10:09
|
||||
|
||||
import apps.rsform.models
|
||||
from django.conf import settings
|
||||
|
@ -35,18 +35,6 @@ class Migration(migrations.Migration):
|
|||
'verbose_name_plural': 'Схемы',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subscription',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Подписки',
|
||||
'verbose_name_plural': 'Подписка',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Constituenta',
|
||||
fields=[
|
||||
|
@ -68,4 +56,17 @@ class Migration(migrations.Migration):
|
|||
'verbose_name_plural': 'Конституенты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subscription',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Подписки',
|
||||
'verbose_name_plural': 'Подписка',
|
||||
'unique_together': {('user', 'item')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -127,6 +127,17 @@ class LibraryItem(Model):
|
|||
def get_absolute_url(self):
|
||||
return f'/api/library/{self.pk}/'
|
||||
|
||||
def subscribers(self) -> list[User]:
|
||||
''' Get all subscribers for this item . '''
|
||||
return [s.user for s in Subscription.objects.filter(item=self.pk)]
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
subscribe = not self.pk and self.owner
|
||||
super().save(*args, **kwargs)
|
||||
if subscribe:
|
||||
Subscription.subscribe(user=self.owner, item=self)
|
||||
|
||||
|
||||
class Subscription(Model):
|
||||
''' User subscription to library item. '''
|
||||
|
@ -145,10 +156,28 @@ class Subscription(Model):
|
|||
''' Model metadata. '''
|
||||
verbose_name = 'Подписки'
|
||||
verbose_name_plural = 'Подписка'
|
||||
unique_together = [['user', 'item']]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.user} -> {self.item}'
|
||||
|
||||
@staticmethod
|
||||
def subscribe(user: User, item: LibraryItem) -> bool:
|
||||
''' Add subscription. '''
|
||||
if Subscription.objects.filter(user=user, item=item).exists():
|
||||
return False
|
||||
Subscription.objects.create(user=user, item=item)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def unsubscribe(user: User, item: LibraryItem) -> bool:
|
||||
''' Remove subscription. '''
|
||||
sub = Subscription.objects.filter(user=user, item=item)
|
||||
if not sub.exists():
|
||||
return False
|
||||
sub.delete()
|
||||
return True
|
||||
|
||||
|
||||
class Constituenta(Model):
|
||||
''' Constituenta is the base unit for every conceptual schema '''
|
||||
|
@ -514,6 +543,7 @@ class PyConceptAdapter:
|
|||
result['time_update'] = self.schema.item.time_update
|
||||
result['time_create'] = self.schema.item.time_create
|
||||
result['is_common'] = self.schema.item.is_common
|
||||
result['is_canonical'] = self.schema.item.is_canonical
|
||||
result['owner'] = (self.schema.item.owner.pk if self.schema.item.owner is not None else None)
|
||||
for cst_data in result['items']:
|
||||
cst = Constituenta.objects.get(pk=cst_data['id'])
|
||||
|
@ -527,6 +557,7 @@ class PyConceptAdapter:
|
|||
'raw': cst.definition_raw,
|
||||
'resolved': cst.definition_resolved,
|
||||
}
|
||||
result['subscribers'] = [item.pk for item in self.schema.item.subscribers()]
|
||||
return result
|
||||
|
||||
def _prepare_request(self) -> dict:
|
||||
|
|
Binary file not shown.
|
@ -5,12 +5,9 @@ from django.db.utils import IntegrityError
|
|||
from django.forms import ValidationError
|
||||
|
||||
from apps.rsform.models import (
|
||||
RSForm,
|
||||
Constituenta,
|
||||
CstType,
|
||||
RSForm, Constituenta, CstType,
|
||||
User,
|
||||
LibraryItem,
|
||||
LibraryItemType
|
||||
LibraryItem, LibraryItemType, Subscription
|
||||
)
|
||||
|
||||
|
||||
|
@ -28,7 +25,7 @@ class TestConstituenta(TestCase):
|
|||
def test_url(self):
|
||||
testStr = 'X1'
|
||||
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test')
|
||||
self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.id}/')
|
||||
self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.id}')
|
||||
|
||||
def test_order_not_null(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
|
@ -110,7 +107,39 @@ class TestLibraryItem(TestCase):
|
|||
self.assertEqual(item.comment, 'Test comment')
|
||||
self.assertEqual(item.is_common, True)
|
||||
self.assertEqual(item.is_canonical, True)
|
||||
self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists())
|
||||
|
||||
def test_subscribe(self):
|
||||
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
|
||||
self.assertEqual(len(item.subscribers()), 0)
|
||||
|
||||
self.assertTrue(Subscription.subscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertTrue(self.user1 in item.subscribers())
|
||||
|
||||
self.assertFalse(Subscription.subscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
|
||||
self.assertTrue(Subscription.subscribe(self.user2, item))
|
||||
self.assertEqual(len(item.subscribers()), 2)
|
||||
self.assertTrue(self.user1 in item.subscribers())
|
||||
self.assertTrue(self.user2 in item.subscribers())
|
||||
|
||||
self.user1.delete()
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
|
||||
def test_unsubscribe(self):
|
||||
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1, item))
|
||||
Subscription.subscribe(self.user1, item)
|
||||
Subscription.subscribe(self.user2, item)
|
||||
self.assertEqual(len(item.subscribers()), 2)
|
||||
|
||||
self.assertTrue(Subscription.unsubscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertTrue(self.user2 in item.subscribers())
|
||||
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1, item))
|
||||
|
||||
class TestRSForm(TestCase):
|
||||
''' Testing RSForm wrapper. '''
|
||||
|
|
|
@ -9,7 +9,7 @@ from rest_framework.exceptions import ErrorDetail
|
|||
from cctext import ReferenceType
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType, LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType, LibraryItem, LibraryItemType, Subscription
|
||||
from apps.rsform.views import (
|
||||
convert_to_ascii,
|
||||
convert_to_math,
|
||||
|
@ -43,28 +43,28 @@ class TestConstituentaAPI(APITestCase):
|
|||
definition_raw='Test1', definition_resolved='Test2')
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(f'/api/constituents/{self.cst1.id}/')
|
||||
response = self.client.get(f'/api/constituents/{self.cst1.id}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['alias'], self.cst1.alias)
|
||||
self.assertEqual(response.data['convention'], self.cst1.convention)
|
||||
|
||||
def test_partial_update(self):
|
||||
data = json.dumps({'convention': 'tt'})
|
||||
response = self.client.patch(f'/api/constituents/{self.cst2.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst2.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.logout()
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.cst1.refresh_from_db()
|
||||
self.assertEqual(response.data['convention'], 'tt')
|
||||
self.assertEqual(self.cst1.convention, 'tt')
|
||||
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_update_resolved_norefs(self):
|
||||
|
@ -72,7 +72,7 @@ class TestConstituentaAPI(APITestCase):
|
|||
'term_raw': 'New term',
|
||||
'definition_raw': 'New def'
|
||||
})
|
||||
response = self.client.patch(f'/api/constituents/{self.cst3.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.cst3.refresh_from_db()
|
||||
self.assertEqual(response.data['term_resolved'], 'New term')
|
||||
|
@ -85,7 +85,7 @@ class TestConstituentaAPI(APITestCase):
|
|||
'term_raw': '@{X1|nomn,sing}',
|
||||
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
|
||||
})
|
||||
response = self.client.patch(f'/api/constituents/{self.cst3.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.cst3.refresh_from_db()
|
||||
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
|
||||
|
@ -95,7 +95,7 @@ class TestConstituentaAPI(APITestCase):
|
|||
|
||||
def test_readonly_cst_fields(self):
|
||||
data = json.dumps({'alias': 'X33', 'order': 10})
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['alias'], 'X1')
|
||||
self.assertEqual(response.data['alias'], self.cst1.alias)
|
||||
|
@ -130,19 +130,19 @@ class TestLibraryViewset(APITestCase):
|
|||
def test_create_anonymous(self):
|
||||
self.client.logout()
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post('/api/library/', data=data, content_type='application/json')
|
||||
response = self.client.post('/api/library', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_create_populate_user(self):
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post('/api/library/', data=data, content_type='application/json')
|
||||
response = self.client.post('/api/library', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['title'], 'Title')
|
||||
self.assertEqual(response.data['owner'], self.user.id)
|
||||
|
||||
def test_update(self):
|
||||
data = json.dumps({'id': self.owned.id, 'title': 'New title'})
|
||||
response = self.client.patch(f'/api/library/{self.owned.id}/',
|
||||
response = self.client.patch(f'/api/library/{self.owned.id}',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['title'], 'New title')
|
||||
|
@ -150,61 +150,91 @@ class TestLibraryViewset(APITestCase):
|
|||
|
||||
def test_update_unowned(self):
|
||||
data = json.dumps({'id': self.unowned.id, 'title': 'New title'})
|
||||
response = self.client.patch(f'/api/library/{self.unowned.id}/',
|
||||
response = self.client.patch(f'/api/library/{self.unowned.id}',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_destroy(self):
|
||||
response = self.client.delete(f'/api/library/{self.owned.id}/')
|
||||
response = self.client.delete(f'/api/library/{self.owned.id}')
|
||||
self.assertTrue(response.status_code in [202, 204])
|
||||
|
||||
def test_destroy_admin_override(self):
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}/')
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}/')
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}')
|
||||
self.assertTrue(response.status_code in [202, 204])
|
||||
|
||||
def test_claim(self):
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.owned.is_common = True
|
||||
self.owned.save()
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim')
|
||||
self.assertEqual(response.status_code, 304)
|
||||
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/claim/')
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/claim')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.assertFalse(self.user in self.unowned.subscribers())
|
||||
self.unowned.is_common = True
|
||||
self.unowned.save()
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/claim/')
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/claim')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.unowned.refresh_from_db()
|
||||
self.assertEqual(self.unowned.owner, self.user)
|
||||
self.assertEqual(self.unowned.owner, self.user)
|
||||
self.assertTrue(self.user in self.unowned.subscribers())
|
||||
|
||||
def test_claim_anonymous(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
|
||||
response = self.client.post(f'/api/library/{self.owned.id}/claim')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_retrieve_common(self):
|
||||
self.client.logout()
|
||||
response = self.client.get('/api/library/active/')
|
||||
response = self.client.get('/api/library/active')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(_response_contains(response, self.common))
|
||||
self.assertFalse(_response_contains(response, self.unowned))
|
||||
self.assertFalse(_response_contains(response, self.owned))
|
||||
|
||||
def test_retrieve_owned(self):
|
||||
response = self.client.get('/api/library/active/')
|
||||
response = self.client.get('/api/library/active')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(_response_contains(response, self.common))
|
||||
self.assertFalse(_response_contains(response, self.unowned))
|
||||
self.assertTrue(_response_contains(response, self.owned))
|
||||
|
||||
def test_retrieve_subscribed(self):
|
||||
response = self.client.get('/api/library/active')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(_response_contains(response, self.unowned))
|
||||
|
||||
Subscription.subscribe(user=self.user, item=self.unowned)
|
||||
response = self.client.get('/api/library/active')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(_response_contains(response, self.unowned))
|
||||
|
||||
def test_subscriptions(self):
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(self.user in self.unowned.subscribers())
|
||||
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/subscribe')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(self.user in self.unowned.subscribers())
|
||||
|
||||
response = self.client.post(f'/api/library/{self.unowned.id}/subscribe')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(self.user in self.unowned.subscribers())
|
||||
|
||||
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(self.user in self.unowned.subscribers())
|
||||
|
||||
|
||||
class TestRSFormViewset(APITestCase):
|
||||
''' Testing RSForm view. '''
|
||||
|
@ -221,13 +251,13 @@ class TestRSFormViewset(APITestCase):
|
|||
item_type=LibraryItemType.OPERATIONS_SCHEMA,
|
||||
title='Test3'
|
||||
)
|
||||
response = self.client.get('/api/rsforms/')
|
||||
response = self.client.get('/api/rsforms')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(_response_contains(response, non_schema))
|
||||
self.assertTrue(_response_contains(response, self.unowned.item))
|
||||
self.assertTrue(_response_contains(response, self.owned.item))
|
||||
|
||||
response = self.client.get('/api/library/')
|
||||
response = self.client.get('/api/library')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(_response_contains(response, non_schema))
|
||||
self.assertTrue(_response_contains(response, self.unowned.item))
|
||||
|
@ -236,11 +266,11 @@ class TestRSFormViewset(APITestCase):
|
|||
def test_contents(self):
|
||||
schema = RSForm.create(title='Title1')
|
||||
schema.insert_last(alias='X1', insert_type=CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/contents/')
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/contents')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_details(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema = RSForm.create(title='Test', owner=self.user)
|
||||
x1 = schema.insert_at(1, 'X1', CstType.BASE)
|
||||
x2 = schema.insert_at(2, 'X2', CstType.BASE)
|
||||
x1.term_raw = 'человек'
|
||||
|
@ -250,7 +280,7 @@ class TestRSFormViewset(APITestCase):
|
|||
x1.save()
|
||||
x2.save()
|
||||
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/details/')
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/details')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['title'], 'Test')
|
||||
|
@ -262,12 +292,13 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(response.data['items'][1]['id'], x2.id)
|
||||
self.assertEqual(response.data['items'][1]['term']['raw'], x2.term_raw)
|
||||
self.assertEqual(response.data['items'][1]['term']['resolved'], x2.term_resolved)
|
||||
self.assertEqual(response.data['subscribers'], [self.user.pk])
|
||||
|
||||
def test_check(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema.insert_at(1, 'X1', CstType.BASE)
|
||||
data = json.dumps({'expression': 'X1=X1'})
|
||||
response = self.client.post(f'/api/rsforms/{schema.item.id}/check/', data=data, content_type='application/json')
|
||||
response = self.client.post(f'/api/rsforms/{schema.item.id}/check', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['parseResult'], True)
|
||||
self.assertEqual(response.data['syntax'], Syntax.MATH)
|
||||
|
@ -281,7 +312,7 @@ class TestRSFormViewset(APITestCase):
|
|||
x1.term_resolved = 'синий слон'
|
||||
x1.save()
|
||||
data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'})
|
||||
response = self.client.post(f'/api/rsforms/{schema.item.id}/resolve/', data=data, content_type='application/json')
|
||||
response = self.client.post(f'/api/rsforms/{schema.item.id}/resolve', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
|
||||
self.assertEqual(response.data['output'], 'редким синим слонам')
|
||||
|
@ -307,7 +338,7 @@ class TestRSFormViewset(APITestCase):
|
|||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file}
|
||||
response = self.client.post('/api/rsforms/import-trs/', data=data, format='multipart')
|
||||
response = self.client.post('/api/rsforms/import-trs', data=data, format='multipart')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertTrue(response.data['title'] != '')
|
||||
|
@ -315,7 +346,7 @@ class TestRSFormViewset(APITestCase):
|
|||
def test_export_trs(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema.insert_at(1, 'X1', CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs/')
|
||||
response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
|
||||
with io.BytesIO(response.content) as stream:
|
||||
|
@ -325,14 +356,14 @@ class TestRSFormViewset(APITestCase):
|
|||
|
||||
def test_create_constituenta(self):
|
||||
data = json.dumps({'alias': 'X3', 'cst_type': 'basic'})
|
||||
response = self.client.post(f'/api/rsforms/{self.unowned.item.id}/cst-create/',
|
||||
response = self.client.post(f'/api/rsforms/{self.unowned.item.id}/cst-create',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
item = self.owned.item
|
||||
Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
|
||||
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['new_cst']['alias'], 'X3')
|
||||
|
@ -340,7 +371,7 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(x3.order, 3)
|
||||
|
||||
data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id})
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['new_cst']['alias'], 'X4')
|
||||
|
@ -361,16 +392,16 @@ class TestRSFormViewset(APITestCase):
|
|||
definition_raw='Test1', definition_resolved='Test2')
|
||||
|
||||
data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk})
|
||||
response = self.client.patch(f'/api/rsforms/{self.unowned.item.id}/cst-rename/',
|
||||
response = self.client.patch(f'/api/rsforms/{self.unowned.item.id}/cst-rename',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename/',
|
||||
response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
data = json.dumps({'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk})
|
||||
response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename/',
|
||||
response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
@ -384,7 +415,7 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(self.cst1.order, 1)
|
||||
self.assertEqual(self.cst1.alias, 'X1')
|
||||
self.assertEqual(self.cst1.cst_type, CstType.BASE)
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-rename/',
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-rename',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['new_cst']['alias'], 'D2')
|
||||
|
@ -407,7 +438,7 @@ class TestRSFormViewset(APITestCase):
|
|||
'definition_raw': '4'
|
||||
})
|
||||
item = self.owned.item
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
|
||||
response = self.client.post(f'/api/rsforms/{item.id}/cst-create',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['new_cst']['alias'], 'X3')
|
||||
|
@ -422,14 +453,14 @@ class TestRSFormViewset(APITestCase):
|
|||
def test_delete_constituenta(self):
|
||||
schema = self.owned
|
||||
data = json.dumps({'items': [{'id': 1337}]})
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
|
||||
x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2)
|
||||
data = json.dumps({'items': [{'id': x1.id}]})
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete',
|
||||
data=data, content_type='application/json')
|
||||
x2.refresh_from_db()
|
||||
schema.item.refresh_from_db()
|
||||
|
@ -441,21 +472,21 @@ class TestRSFormViewset(APITestCase):
|
|||
|
||||
x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1)
|
||||
data = json.dumps({'items': [{'id': x3.id}]})
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_move_constituenta(self):
|
||||
item = self.owned.item
|
||||
data = json.dumps({'items': [{'id': 1337}], 'move_to': 1})
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
|
||||
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
|
||||
data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1})
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto',
|
||||
data=data, content_type='application/json')
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
|
@ -466,20 +497,20 @@ class TestRSFormViewset(APITestCase):
|
|||
|
||||
x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1)
|
||||
data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1})
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto',
|
||||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_aliases(self):
|
||||
item = self.owned.item
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['id'], item.id)
|
||||
|
||||
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1)
|
||||
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=2)
|
||||
d11 = Constituenta.objects.create(schema=item, alias='D11', cst_type='term', order=3)
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases')
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
d11.refresh_from_db()
|
||||
|
@ -491,7 +522,7 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(d11.order, 3)
|
||||
self.assertEqual(d11.alias, 'D1')
|
||||
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
|
||||
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_load_trs(self):
|
||||
|
@ -502,13 +533,13 @@ class TestRSFormViewset(APITestCase):
|
|||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file, 'load_metadata': False}
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/load-trs/', data=data, format='multipart')
|
||||
response = self.client.patch(f'/api/rsforms/{schema.item.id}/load-trs', data=data, format='multipart')
|
||||
schema.item.refresh_from_db()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(schema.item.title, 'Testt11')
|
||||
self.assertEqual(len(response.data['items']), 25)
|
||||
self.assertEqual(schema.constituents().count(), 25)
|
||||
self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists())
|
||||
self.assertFalse(Constituenta.objects.filter(pk=x1.id).exists())
|
||||
|
||||
def test_clone(self):
|
||||
item = self.owned.item
|
||||
|
@ -524,7 +555,7 @@ class TestRSFormViewset(APITestCase):
|
|||
d1.save()
|
||||
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post(f'/api/library/{item.id}/clone/', data=data, content_type='application/json')
|
||||
response = self.client.post(f'/api/library/{item.id}/clone', data=data, content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['title'], 'Title')
|
||||
|
@ -546,7 +577,7 @@ class TestFunctionalViews(APITestCase):
|
|||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
|
||||
response = self.client.post('/api/rsforms/create-detailed/', data=data, format='multipart')
|
||||
response = self.client.post('/api/rsforms/create-detailed', data=data, format='multipart')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['title'], 'Test123')
|
||||
|
@ -555,7 +586,7 @@ class TestFunctionalViews(APITestCase):
|
|||
|
||||
def test_create_rsform_fallback(self):
|
||||
data = {'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
|
||||
response = self.client.post('/api/rsforms/create-detailed/', data=data)
|
||||
response = self.client.post('/api/rsforms/create-detailed', data=data)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['title'], 'Test123')
|
||||
|
|
|
@ -3,17 +3,17 @@ from django.urls import path, include
|
|||
from rest_framework import routers
|
||||
from . import views
|
||||
|
||||
library_router = routers.SimpleRouter()
|
||||
library_router = routers.SimpleRouter(trailing_slash=False)
|
||||
library_router.register('library', views.LibraryViewSet)
|
||||
library_router.register('rsforms', views.RSFormViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('library/active/', views.LibraryActiveView.as_view(), name='library'),
|
||||
path('constituents/<int:pk>/', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
|
||||
path('rsforms/import-trs/', views.TrsImportView.as_view()),
|
||||
path('rsforms/create-detailed/', views.create_rsform),
|
||||
path('func/parse-expression/', views.parse_expression),
|
||||
path('func/to-ascii/', views.convert_to_ascii),
|
||||
path('func/to-math/', views.convert_to_math),
|
||||
path('library/active', views.LibraryActiveView.as_view(), name='library'),
|
||||
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
|
||||
path('rsforms/import-trs', views.TrsImportView.as_view()),
|
||||
path('rsforms/create-detailed', views.create_rsform),
|
||||
path('func/parse-expression', views.parse_expression),
|
||||
path('func/to-ascii', views.convert_to_ascii),
|
||||
path('func/to-math', views.convert_to_math),
|
||||
path('', include(library_router.urls)),
|
||||
]
|
||||
|
|
|
@ -24,7 +24,8 @@ class LibraryActiveView(generics.ListAPIView):
|
|||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if not user.is_anonymous:
|
||||
return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user))
|
||||
# pyling: disable=unsupported-binary-operation
|
||||
return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user))
|
||||
else:
|
||||
return m.LibraryItem.objects.filter(is_common=True)
|
||||
|
||||
|
@ -62,7 +63,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
def get_permissions(self):
|
||||
if self.action in ['update', 'destroy', 'partial_update']:
|
||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
||||
elif self.action in ['create', 'clone']:
|
||||
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
elif self.action in ['claim']:
|
||||
permission_classes = [utils.IsClaimable]
|
||||
|
@ -70,13 +71,16 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
permission_classes = [permissions.AllowAny]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def _get_item(self) -> m.LibraryItem:
|
||||
return cast(m.LibraryItem, self.get_object())
|
||||
|
||||
@transaction.atomic
|
||||
@action(detail=True, methods=['post'], url_path='clone')
|
||||
def clone(self, request, pk):
|
||||
''' Endpoint: Create deep copy of library item. '''
|
||||
serializer = s.LibraryItemSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
item = cast(m.LibraryItem, self.get_object())
|
||||
item = self._get_item()
|
||||
if item.item_type == m.LibraryItemType.RSFORM:
|
||||
schema = m.RSForm(item)
|
||||
clone_data = s.RSFormTRSSerializer(schema).data
|
||||
|
@ -93,21 +97,37 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
return Response(status=201, data=m.PyConceptAdapter(new_schema).full())
|
||||
return Response(status=404)
|
||||
|
||||
@transaction.atomic
|
||||
@action(detail=True, methods=['post'])
|
||||
def claim(self, request, pk=None):
|
||||
''' Endpoint: Claim ownership of LibraryItem. '''
|
||||
item = cast(m.LibraryItem, self.get_object())
|
||||
item = self._get_item()
|
||||
if item.owner == self.request.user:
|
||||
return Response(status=304)
|
||||
else:
|
||||
item.owner = self.request.user
|
||||
item.save()
|
||||
m.Subscription.subscribe(user=item.owner, item=item)
|
||||
return Response(status=200, data=s.LibraryItemSerializer(item).data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def subscribe(self, request, pk):
|
||||
''' Endpoint: Subscribe current user to item. '''
|
||||
item = self._get_item()
|
||||
m.Subscription.subscribe(user=self.request.user, item=item)
|
||||
return Response(status=200)
|
||||
|
||||
@action(detail=True, methods=['delete'])
|
||||
def unsubscribe(self, request, pk):
|
||||
''' Endpoint: Unsubscribe current user from item. '''
|
||||
item = self._get_item()
|
||||
m.Subscription.unsubscribe(user=self.request.user, item=item)
|
||||
return Response(status=200)
|
||||
|
||||
|
||||
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
|
||||
''' Endpoint: RSForm operations. '''
|
||||
queryset = m.LibraryItem.objects.all().filter(item_type=m.LibraryItemType.RSFORM)
|
||||
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM)
|
||||
serializer_class = s.LibraryItemSerializer
|
||||
|
||||
def _get_schema(self) -> m.RSForm:
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.auth import authenticate
|
|||
from django.contrib.auth.password_validation import validate_password
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.rsform.models import Subscription
|
||||
from . import models
|
||||
|
||||
|
||||
|
@ -40,16 +41,23 @@ class LoginSerializer(serializers.Serializer):
|
|||
raise NotImplementedError('unexpected `update()` call')
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
class AuthSerializer(serializers.Serializer):
|
||||
''' Serializer: Authentication data. '''
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = models.User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'is_staff'
|
||||
]
|
||||
def to_representation(self, instance: models.User) -> dict:
|
||||
if instance.is_anonymous:
|
||||
return {
|
||||
'id': None,
|
||||
'username': '',
|
||||
'is_staff': False,
|
||||
'subscriptions': []
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'id': instance.pk,
|
||||
'username': instance.username,
|
||||
'is_staff': instance.is_staff,
|
||||
'subscriptions': [sub.item.pk for sub in Subscription.objects.filter(user=instance)]
|
||||
}
|
||||
|
||||
|
||||
class UserInfoSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -3,9 +3,10 @@ import json
|
|||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.rsform.models import LibraryItem, LibraryItemType
|
||||
|
||||
|
||||
# TODO: test AUTH and ATIVE_USERS
|
||||
# TODO: test ACTIVE_USERS
|
||||
class TestUserAPIViews(APITestCase):
|
||||
def setUp(self):
|
||||
self.username = 'UserTest'
|
||||
|
@ -30,6 +31,30 @@ class TestUserAPIViews(APITestCase):
|
|||
|
||||
self.assertEqual(self.client.post('/users/api/logout').status_code, 403)
|
||||
|
||||
def test_auth(self):
|
||||
LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='T1')
|
||||
item = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
title='Test',
|
||||
alias='T1',
|
||||
is_common=True,
|
||||
owner=self.user
|
||||
)
|
||||
response = self.client.get('/users/api/auth')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['id'], None)
|
||||
self.assertEqual(response.data['username'], '')
|
||||
self.assertEqual(response.data['is_staff'], False)
|
||||
self.assertEqual(response.data['subscriptions'], [])
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get('/users/api/auth')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['id'], self.user.pk)
|
||||
self.assertEqual(response.data['username'], self.user.username)
|
||||
self.assertEqual(response.data['is_staff'], self.user.is_staff)
|
||||
self.assertEqual(response.data['subscriptions'], [item.pk])
|
||||
|
||||
|
||||
class TestUserUserProfileAPIView(APITestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -8,9 +8,7 @@ from . import serializers
|
|||
from . import models
|
||||
|
||||
class LoginAPIView(views.APIView):
|
||||
'''
|
||||
Endpoint: Login user via username + password.
|
||||
'''
|
||||
''' Endpoint: Login via username + password. '''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
|
@ -25,9 +23,7 @@ class LoginAPIView(views.APIView):
|
|||
|
||||
|
||||
class LogoutAPIView(views.APIView):
|
||||
'''
|
||||
Endpoint: Logout current user.
|
||||
'''
|
||||
''' Endpoint: Logout. '''
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def post(self, request):
|
||||
|
@ -36,17 +32,13 @@ class LogoutAPIView(views.APIView):
|
|||
|
||||
|
||||
class SignupAPIView(generics.CreateAPIView):
|
||||
'''
|
||||
Register user.
|
||||
'''
|
||||
''' Endpoint: Register user. '''
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
serializer_class = serializers.SignupSerializer
|
||||
|
||||
|
||||
class AuthAPIView(generics.RetrieveAPIView):
|
||||
'''
|
||||
Get current user authentification ID.
|
||||
'''
|
||||
''' Endpoint: Current user info. '''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.AuthSerializer
|
||||
|
||||
|
@ -55,9 +47,7 @@ class AuthAPIView(generics.RetrieveAPIView):
|
|||
|
||||
|
||||
class ActiveUsersView(generics.ListAPIView):
|
||||
'''
|
||||
Endpoint: Get list of active users.
|
||||
'''
|
||||
''' Endpoint: Get list of active users. '''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
|
@ -66,9 +56,7 @@ class ActiveUsersView(generics.ListAPIView):
|
|||
|
||||
|
||||
class UserProfileAPIView(generics.RetrieveUpdateAPIView):
|
||||
'''
|
||||
Endpoint: User profile info.
|
||||
'''
|
||||
''' Endpoint: User profile. '''
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
|
@ -77,9 +65,7 @@ class UserProfileAPIView(generics.RetrieveUpdateAPIView):
|
|||
|
||||
|
||||
class UpdatePassword(views.APIView):
|
||||
'''
|
||||
Endpoint: Change password for current user.
|
||||
'''
|
||||
''' Endpoint: Change password for current user. '''
|
||||
permission_classes = (permissions.IsAuthenticated, )
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
"alias": "M0005",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.052Z",
|
||||
"time_update": "2023-08-25T19:03:40.052Z"
|
||||
}
|
||||
|
@ -174,7 +174,7 @@
|
|||
"alias": "M0006",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.066Z",
|
||||
"time_update": "2023-08-25T19:03:40.066Z"
|
||||
}
|
||||
|
@ -189,7 +189,7 @@
|
|||
"alias": "M0007",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.077Z",
|
||||
"time_update": "2023-08-25T19:03:40.077Z"
|
||||
}
|
||||
|
@ -204,7 +204,7 @@
|
|||
"alias": "M0008",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.081Z",
|
||||
"time_update": "2023-08-25T19:03:40.081Z"
|
||||
}
|
||||
|
@ -219,7 +219,7 @@
|
|||
"alias": "M0009",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.094Z",
|
||||
"time_update": "2023-08-25T19:03:40.095Z"
|
||||
}
|
||||
|
@ -234,7 +234,7 @@
|
|||
"alias": "M0010",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.118Z",
|
||||
"time_update": "2023-08-25T19:03:40.118Z"
|
||||
}
|
||||
|
@ -249,7 +249,7 @@
|
|||
"alias": "M0011",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.132Z",
|
||||
"time_update": "2023-08-25T19:03:40.132Z"
|
||||
}
|
||||
|
@ -264,7 +264,7 @@
|
|||
"alias": "M0012",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.152Z",
|
||||
"time_update": "2023-08-25T19:03:40.152Z"
|
||||
}
|
||||
|
@ -279,7 +279,7 @@
|
|||
"alias": "M0013",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.167Z",
|
||||
"time_update": "2023-08-25T19:03:40.168Z"
|
||||
}
|
||||
|
@ -294,7 +294,7 @@
|
|||
"alias": "M0014",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.196Z",
|
||||
"time_update": "2023-08-25T19:03:40.196Z"
|
||||
}
|
||||
|
@ -309,7 +309,7 @@
|
|||
"alias": "M0015",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.207Z",
|
||||
"time_update": "2023-08-25T19:03:40.207Z"
|
||||
}
|
||||
|
@ -324,7 +324,7 @@
|
|||
"alias": "M0016",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.258Z",
|
||||
"time_update": "2023-08-25T19:03:40.258Z"
|
||||
}
|
||||
|
@ -354,7 +354,7 @@
|
|||
"alias": "D0002",
|
||||
"comment": "",
|
||||
"is_common": true,
|
||||
"is_canonical": true,
|
||||
"is_canonical": false,
|
||||
"time_create": "2023-08-25T19:03:40.459Z",
|
||||
"time_update": "2023-08-25T19:03:40.460Z"
|
||||
}
|
||||
|
|
|
@ -3,14 +3,15 @@ import { TextareaHTMLAttributes } from 'react';
|
|||
import Label from './Label';
|
||||
|
||||
export interface TextAreaProps
|
||||
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
|
||||
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className' | 'title'> {
|
||||
label: string
|
||||
tooltip?: string
|
||||
widthClass?: string
|
||||
colorClass?: string
|
||||
}
|
||||
|
||||
function TextArea({
|
||||
id, label, required,
|
||||
id, label, required, tooltip,
|
||||
widthClass = 'w-full',
|
||||
colorClass = 'clr-input',
|
||||
rows = 4,
|
||||
|
@ -24,6 +25,7 @@ function TextArea({
|
|||
htmlFor={id}
|
||||
/>
|
||||
<textarea id={id}
|
||||
title={tooltip}
|
||||
className={`px-3 py-2 mt-2 leading-tight border shadow ${colorClass} ${widthClass}`}
|
||||
rows={rows}
|
||||
required={required}
|
||||
|
|
|
@ -3,16 +3,17 @@ import { type InputHTMLAttributes } from 'react';
|
|||
import Label from './Label';
|
||||
|
||||
interface TextInputProps
|
||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> {
|
||||
id: string
|
||||
label: string
|
||||
tooltip?: string
|
||||
widthClass?: string
|
||||
colorClass?: string
|
||||
singleRow?: boolean
|
||||
}
|
||||
|
||||
function TextInput({
|
||||
id, required, label, singleRow,
|
||||
id, required, label, singleRow, tooltip,
|
||||
widthClass = 'w-full',
|
||||
colorClass = 'clr-input',
|
||||
...props
|
||||
|
@ -25,6 +26,7 @@ function TextInput({
|
|||
htmlFor={id}
|
||||
/>
|
||||
<input id={id}
|
||||
title={tooltip}
|
||||
className={`px-3 py-2 mt-2 leading-tight border shadow truncate hover:text-clip ${colorClass} ${singleRow ? '' : widthClass}`}
|
||||
required={required}
|
||||
{...props}
|
||||
|
|
|
@ -44,7 +44,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
(params: ILibraryFilter) => {
|
||||
let result = items;
|
||||
if (params.ownedBy) {
|
||||
result = result.filter(item => item.owner === params.ownedBy);
|
||||
result = result.filter(item =>
|
||||
item.owner === params.ownedBy
|
||||
|| user?.subscriptions.includes(item.id));
|
||||
}
|
||||
if (params.is_common !== undefined) {
|
||||
result = result.filter(item => item.is_common === params.is_common);
|
||||
|
@ -53,7 +55,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
result = result.filter(item => matchLibraryItem(params.queryMeta!, item));
|
||||
}
|
||||
return result;
|
||||
}, [items]);
|
||||
}, [items, user]);
|
||||
|
||||
const reload = useCallback(
|
||||
(callback?: () => void) => {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { type ErrorInfo } from '../components/BackendError'
|
||||
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
||||
import {
|
||||
type DataCallback, getTRSFile,
|
||||
type DataCallback, deleteUnsubscribe,
|
||||
getTRSFile,
|
||||
patchConstituenta, patchDeleteConstituenta,
|
||||
patchLibraryItem,
|
||||
patchMoveConstituenta, patchRenameConstituenta,
|
||||
patchResetAliases, patchUploadTRS, postClaimLibraryItem, postNewConstituenta
|
||||
} from '../utils/backendAPI'
|
||||
patchResetAliases, patchUploadTRS, postClaimLibraryItem, postNewConstituenta, postSubscribe} from '../utils/backendAPI'
|
||||
import {
|
||||
IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||
ICstMovetoData, ICstRenameData, ICstUpdateData, ILibraryItem,
|
||||
|
@ -33,10 +32,11 @@ interface IRSFormContext {
|
|||
|
||||
toggleForceAdmin: () => void
|
||||
toggleReadonly: () => void
|
||||
toggleTracking: () => void
|
||||
|
||||
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void
|
||||
claim: (callback?: DataCallback<ILibraryItem>) => void
|
||||
subscribe: (callback?: () => void) => void
|
||||
unsubscribe: (callback?: () => void) => void
|
||||
download: (callback: DataCallback<Blob>) => void
|
||||
upload: (data: IRSFormUploadData, callback: () => void) => void
|
||||
|
||||
|
@ -72,6 +72,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
|
||||
const [ isForceAdmin, setIsForceAdmin ] = useState(false);
|
||||
const [ isReadonly, setIsReadonly ] = useState(false);
|
||||
const [ toggleTracking, setToggleTracking ] = useState(false);
|
||||
|
||||
const isOwned = useMemo(
|
||||
() => {
|
||||
|
@ -93,13 +94,12 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
|
||||
const isTracking = useMemo(
|
||||
() => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const toggleTracking = useCallback(
|
||||
() => {
|
||||
toast.info('Отслеживание в разработке...')
|
||||
}, []);
|
||||
if (!schema || !user) {
|
||||
return false;
|
||||
}
|
||||
return schema.subscribers.includes(user.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, schema, toggleTracking]);
|
||||
|
||||
const update = useCallback(
|
||||
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
|
||||
|
@ -154,6 +154,52 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
});
|
||||
}, [schemaID, setError, schema, user, setSchema]);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(callback?: () => void) => {
|
||||
if (!schema || !user) {
|
||||
return;
|
||||
}
|
||||
setError(undefined)
|
||||
postSubscribe(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => setError(error),
|
||||
onSuccess: () => {
|
||||
if (!schema.subscribers.includes(user.id)) {
|
||||
schema.subscribers.push(user.id);
|
||||
}
|
||||
if (!user.subscriptions.includes(schema.id)) {
|
||||
user.subscriptions.push(schema.id);
|
||||
}
|
||||
setToggleTracking(prev => !prev);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user]);
|
||||
|
||||
const unsubscribe = useCallback(
|
||||
(callback?: () => void) => {
|
||||
if (!schema || !user) {
|
||||
return;
|
||||
}
|
||||
setError(undefined)
|
||||
deleteUnsubscribe(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => setError(error),
|
||||
onSuccess: () => {
|
||||
if (schema.subscribers.includes(user.id)) {
|
||||
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
|
||||
}
|
||||
if (user.subscriptions.includes(schema.id)) {
|
||||
user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1);
|
||||
}
|
||||
setToggleTracking(prev => !prev);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user]);
|
||||
|
||||
const resetAliases = useCallback(
|
||||
(callback?: () => void) => {
|
||||
if (!schema || !user) {
|
||||
|
@ -264,8 +310,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
isClaimable, isTracking,
|
||||
toggleForceAdmin: () => setIsForceAdmin(prev => !prev),
|
||||
toggleReadonly: () => setIsReadonly(prev => !prev),
|
||||
toggleTracking,
|
||||
update, download, upload, claim, resetAliases,
|
||||
update, download, upload, claim, resetAliases, subscribe, unsubscribe,
|
||||
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo
|
||||
}}>
|
||||
{ children }
|
||||
|
|
|
@ -110,6 +110,10 @@
|
|||
@apply bg-white dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500
|
||||
}
|
||||
|
||||
.clr-input-red {
|
||||
@apply bg-red-300 dark:bg-red-700
|
||||
}
|
||||
|
||||
.text-url {
|
||||
@apply hover:text-blue-600 text-blue-400 dark:text-orange-600 dark:hover:text-orange-400
|
||||
}
|
||||
|
|
|
@ -4,8 +4,11 @@ import { useNavigate } from 'react-router-dom';
|
|||
|
||||
import ConceptDataTable from '../../components/Common/ConceptDataTable';
|
||||
import TextURL from '../../components/Common/TextURL';
|
||||
import { EducationIcon, EyeIcon, GroupIcon } from '../../components/Icons';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useNavSearch } from '../../context/NavSearchContext';
|
||||
import { useUsers } from '../../context/UsersContext';
|
||||
import { prefixes } from '../../utils/constants';
|
||||
import { ILibraryItem } from '../../utils/models'
|
||||
|
||||
interface ViewLibraryProps {
|
||||
|
@ -14,14 +17,35 @@ interface ViewLibraryProps {
|
|||
|
||||
function ViewLibrary({ items }: ViewLibraryProps) {
|
||||
const { resetQuery: cleanQuery } = useNavSearch();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const { getUserLabel } = useUsers();
|
||||
|
||||
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`);
|
||||
|
||||
const columns = useMemo(() =>
|
||||
[
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: '',
|
||||
id: 'status',
|
||||
minWidth: '60px',
|
||||
maxWidth: '60px',
|
||||
cell: (item: ILibraryItem) => {
|
||||
return (<>
|
||||
<div
|
||||
className='flex items-center justify-start gap-1'
|
||||
id={`${prefixes.library_list}${item.id}`}
|
||||
>
|
||||
{user && user.subscriptions.includes(item.id) && <EyeIcon size={3}/>}
|
||||
{item.is_common && <GroupIcon size={3}/>}
|
||||
{item.is_canonical && <EducationIcon size={3}/>}
|
||||
</div>
|
||||
</>);
|
||||
},
|
||||
sortable: true,
|
||||
reorder: true
|
||||
},
|
||||
{
|
||||
name: 'Шифр',
|
||||
id: 'alias',
|
||||
|
@ -30,16 +54,6 @@ function ViewLibrary({ items }: ViewLibraryProps) {
|
|||
sortable: true,
|
||||
reorder: true
|
||||
},
|
||||
{
|
||||
name: 'Статусы',
|
||||
id: 'status',
|
||||
maxWidth: '50px',
|
||||
selector: (item: ILibraryItem) => {
|
||||
return `${item.is_canonical ? 'C': ''}${item.is_common ? 'S': ''}`
|
||||
},
|
||||
sortable: true,
|
||||
reorder: true
|
||||
},
|
||||
{
|
||||
name: 'Название',
|
||||
id: 'title',
|
||||
|
@ -66,7 +80,7 @@ function ViewLibrary({ items }: ViewLibraryProps) {
|
|||
sortable: true,
|
||||
reorder: true
|
||||
}
|
||||
], [intl, getUserLabel]);
|
||||
], [intl, getUserLabel, user]);
|
||||
|
||||
return (
|
||||
<ConceptDataTable
|
||||
|
|
|
@ -175,7 +175,6 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
{
|
||||
name: 'Имя',
|
||||
id: 'alias',
|
||||
selector: (cst: IConstituenta) => cst.alias,
|
||||
cell: (cst: IConstituenta) => {
|
||||
const info = mapStatusInfo.get(cst.status)!;
|
||||
return (<>
|
||||
|
|
|
@ -161,6 +161,12 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
|
|||
{getUserLabel(schema?.owner ?? null)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-start mt-2'>
|
||||
<label className='font-semibold'>Отслеживают:</label>
|
||||
<span id='subscriber-count' className='ml-2'>
|
||||
{ schema?.subscribers.length ?? 0 }
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-start mt-2'>
|
||||
<label className='font-semibold'>Дата обновления:</label>
|
||||
<span className='ml-2'>{schema && new Date(schema?.time_update).toLocaleString(intl.locale)}</span>
|
||||
|
|
|
@ -176,7 +176,6 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
|||
});
|
||||
}
|
||||
});
|
||||
console.log(result);
|
||||
return result;
|
||||
}, [schema, coloringScheme, filtered.nodes, darkMode, noTerms]);
|
||||
|
||||
|
@ -194,7 +193,6 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
|||
edgeID += 1;
|
||||
});
|
||||
});
|
||||
console.log(result);
|
||||
return result;
|
||||
}, [filtered.nodes]);
|
||||
|
||||
|
@ -256,7 +254,6 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
|||
|
||||
// Implement hotkeys for editing
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
console.log(event);
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ function RSTabs() {
|
|||
const navigate = useNavigate();
|
||||
const search = useLocation().search;
|
||||
const {
|
||||
error, schema, loading, claim, download,
|
||||
cstCreate, cstDelete, cstRename
|
||||
error, schema, loading, claim, download, isTracking,
|
||||
cstCreate, cstDelete, cstRename, subscribe, unsubscribe
|
||||
} = useRSForm();
|
||||
const { destroySchema } = useLibrary();
|
||||
|
||||
|
@ -225,6 +225,21 @@ function RSTabs() {
|
|||
});
|
||||
}, [schema?.alias, download]);
|
||||
|
||||
const handleToggleSubscribe = useCallback(
|
||||
() => {
|
||||
if (isTracking) {
|
||||
unsubscribe(
|
||||
() => {
|
||||
toast.success('Отслеживание отключено');
|
||||
});
|
||||
} else {
|
||||
subscribe(
|
||||
() => {
|
||||
toast.success('Отслеживание включено');
|
||||
});
|
||||
}
|
||||
}, [isTracking, subscribe, unsubscribe]);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{ loading && <Loader /> }
|
||||
|
@ -274,6 +289,7 @@ function RSTabs() {
|
|||
onDestroy={onDestroySchema}
|
||||
onClaim={onClaimSchema}
|
||||
onShare={onShareSchema}
|
||||
onToggleSubscribe={handleToggleSubscribe}
|
||||
showCloneDialog={() => setShowClone(true)}
|
||||
showUploadDialog={() => setShowUpload(true)}
|
||||
/>
|
||||
|
|
|
@ -16,17 +16,18 @@ interface RSTabsMenuProps {
|
|||
onClaim: () => void
|
||||
onShare: () => void
|
||||
onDownload: () => void
|
||||
onToggleSubscribe: () => void
|
||||
}
|
||||
|
||||
function RSTabsMenu({
|
||||
showUploadDialog, showCloneDialog,
|
||||
onDestroy, onShare, onDownload, onClaim
|
||||
onDestroy, onShare, onDownload, onClaim, onToggleSubscribe
|
||||
}: RSTabsMenuProps) {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin,
|
||||
toggleTracking, toggleForceAdmin, toggleReadonly
|
||||
isOwned, isEditable, isTracking, isReadonly, isClaimable, isForceAdmin,
|
||||
toggleForceAdmin, toggleReadonly, processing
|
||||
} = useRSForm();
|
||||
const schemaMenu = useDropdown();
|
||||
const editMenu = useDropdown();
|
||||
|
@ -127,7 +128,11 @@ function RSTabsMenu({
|
|||
/>
|
||||
{ editMenu.isActive &&
|
||||
<Dropdown>
|
||||
<DropdownButton disabled={!user} onClick={!isOwned ? handleClaimOwner : undefined}>
|
||||
<DropdownButton
|
||||
disabled={!user || !isClaimable}
|
||||
onClick={!isOwned ? handleClaimOwner : undefined}
|
||||
description={!user || !isClaimable ? 'Стать владельцем можно только для общей небиблиотечной схемы' : ''}
|
||||
>
|
||||
<div className='inline-flex items-center gap-1 justify-normal'>
|
||||
<span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span>
|
||||
<p>
|
||||
|
@ -139,20 +144,21 @@ function RSTabsMenu({
|
|||
{(isOwned || user?.is_staff) &&
|
||||
<DropdownButton onClick={toggleReadonly}>
|
||||
<Checkbox
|
||||
value={readonly}
|
||||
value={isReadonly}
|
||||
label='Я — читатель!'
|
||||
tooltip='Режим чтения'
|
||||
/>
|
||||
</DropdownButton>}
|
||||
{user?.is_staff &&
|
||||
<DropdownButton onClick={toggleForceAdmin}>
|
||||
<Checkbox value={forceAdmin} label='режим администратора'/>
|
||||
<Checkbox value={isForceAdmin} label='режим администратора'/>
|
||||
</DropdownButton>}
|
||||
</Dropdown>}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')}
|
||||
disabled={processing}
|
||||
icon={isTracking
|
||||
? <EyeIcon color='text-primary' size={5}/>
|
||||
: <EyeOffIcon size={5}/>
|
||||
|
@ -160,7 +166,7 @@ function RSTabsMenu({
|
|||
widthClass='h-full w-fit'
|
||||
borderClass=''
|
||||
dense
|
||||
onClick={toggleTracking}
|
||||
onClick={onToggleSubscribe}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,60 +1,73 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import BackendError from '../../components/BackendError';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import TextInput from '../../components/Common/TextInput';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { IUserUpdatePassword } from '../../utils/models';
|
||||
|
||||
|
||||
export function ChangePassword() {
|
||||
const { updatePassword, error, loading } = useAuth();
|
||||
function EditorPassword() {
|
||||
const { updatePassword, error, setError, loading } = useAuth();
|
||||
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const colorClass = useMemo(() => {
|
||||
return !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'bg-red-500' : 'clr-input';
|
||||
const passwordColor = useMemo(
|
||||
() => {
|
||||
return !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'clr-input-red' : 'clr-input';
|
||||
}, [newPassword, newPasswordRepeat]);
|
||||
|
||||
const canSubmit = useMemo(
|
||||
() => {
|
||||
return !!oldPassword && !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
|
||||
}, [newPassword, newPasswordRepeat, oldPassword]);
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (newPassword !== newPasswordRepeat) {
|
||||
toast.error('Пароли не совпадают');
|
||||
return;
|
||||
}
|
||||
else {
|
||||
const data: IUserUpdatePassword = {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
};
|
||||
updatePassword(data, () => {toast.success('Изменения сохранены'); navigate('/login')});
|
||||
}
|
||||
updatePassword(data, () => {
|
||||
toast.success('Изменения сохранены');
|
||||
navigate('/login')
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [newPassword, oldPassword, newPasswordRepeat, setError]);
|
||||
|
||||
return (
|
||||
<div className='flex max-w-sm px-4 border-l-2'>
|
||||
<div className='flex py-2 border-l-2 clr-border max-w-[14rem]'>
|
||||
<form onSubmit={handleSubmit} className='flex flex-col justify-between px-6 min-w-fit'>
|
||||
<div>
|
||||
<TextInput id='old_password'
|
||||
type='password'
|
||||
label='Введите старый пароль:'
|
||||
label='Старый пароль'
|
||||
value={oldPassword}
|
||||
onChange={event => setOldPassword(event.target.value)}
|
||||
/>
|
||||
<TextInput id='new_password'
|
||||
colorClass={colorClass}
|
||||
label="Введите новый пароль:"
|
||||
<TextInput id='new_password' type='password'
|
||||
colorClass={passwordColor}
|
||||
label="Новый пароль"
|
||||
value={newPassword}
|
||||
onChange={event => {
|
||||
setNewPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextInput id='new_password_repeat'
|
||||
colorClass={colorClass}
|
||||
label="Повторите новый пароль:"
|
||||
<TextInput id='new_password_repeat' type='password'
|
||||
colorClass={passwordColor}
|
||||
label="Повторите новый"
|
||||
value={newPasswordRepeat}
|
||||
onChange={event => {
|
||||
setNewPasswordRepeat(event.target.value);
|
||||
|
@ -62,14 +75,16 @@ export function ChangePassword() {
|
|||
/>
|
||||
</div>
|
||||
{ error && <BackendError error={error} />}
|
||||
<div className='flex justify-center py-4'>
|
||||
<button
|
||||
type='submit'
|
||||
className={`px-2 py-1 border clr-btn-blue`}
|
||||
disabled={loading}>
|
||||
<span>Сменить пароль</span>
|
||||
</button>
|
||||
<div className='flex justify-center w-full'>
|
||||
<SubmitButton
|
||||
disabled={!canSubmit}
|
||||
loading={loading}
|
||||
text='Сменить пароль'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
export default EditorPassword;
|
|
@ -0,0 +1,68 @@
|
|||
import { useLayoutEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import TextInput from '../../components/Common/TextInput';
|
||||
import { useUserProfile } from '../../context/UserProfileContext';
|
||||
import { IUserUpdateData } from '../../utils/models';
|
||||
|
||||
function EditorProfile() {
|
||||
const { updateUser, user, processing } = useUserProfile();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [first_name, setFirstName] = useState('');
|
||||
const [last_name, setLastName] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (user) {
|
||||
setUsername(user.username);
|
||||
setEmail(user.email);
|
||||
setFirstName(user.first_name);
|
||||
setLastName(user.last_name);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const data: IUserUpdateData = {
|
||||
username: username,
|
||||
email: email,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
};
|
||||
updateUser(data, () => toast.success('Изменения сохранены'));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex py-2'>
|
||||
<form onSubmit={handleSubmit} className='px-6 min-w-[18rem]'>
|
||||
<div>
|
||||
<TextInput id='username'
|
||||
label='Логин'
|
||||
tooltip='Логин изменить нельзя'
|
||||
disabled={true}
|
||||
value={username}
|
||||
onChange={event => setUsername(event.target.value)}
|
||||
/>
|
||||
<TextInput id='first_name'
|
||||
label="Имя"
|
||||
value={first_name}
|
||||
onChange={event => setFirstName(event.target.value)}
|
||||
/>
|
||||
<TextInput id='last_name' label="Фамилия" value={last_name} onChange={event => setLastName(event.target.value)}/>
|
||||
<TextInput id='email' label="Электронная почта" value={email} onChange={event => setEmail(event.target.value)}/>
|
||||
</div>
|
||||
<div className='flex justify-center w-full mt-10'>
|
||||
<SubmitButton
|
||||
text='Сохранить данные'
|
||||
loading={processing}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditorProfile;
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { useLayoutEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import TextInput from '../../components/Common/TextInput';
|
||||
import { useUserProfile } from '../../context/UserProfileContext';
|
||||
import { IUserUpdateData } from '../../utils/models';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
|
||||
|
||||
export function UserProfile() {
|
||||
const { updateUser, user, processing } = useUserProfile();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [first_name, setFirstName] = useState('');
|
||||
const [last_name, setLastName] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (user) {
|
||||
setUsername(user.username);
|
||||
setEmail(user.email);
|
||||
setFirstName(user.first_name);
|
||||
setLastName(user.last_name);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const data: IUserUpdateData = {
|
||||
username: username,
|
||||
email: email,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
};
|
||||
updateUser(data, () => toast.success('Изменения сохранены'));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-center'>
|
||||
<div className='place-self-center'>
|
||||
<h1 className='flex justify-center py-2'> Учетные данные пользователя </h1>
|
||||
<div className='flex justify-center px-6 py-2 max-w-fit'>
|
||||
<div className='flex max-w-sm px-4'>
|
||||
<form onSubmit={handleSubmit} className='px-6 min-w-fit'>
|
||||
<div>
|
||||
<TextInput id='username'
|
||||
label='Логин:'
|
||||
value={username}
|
||||
onChange={event => setUsername(event.target.value)}
|
||||
/>
|
||||
<TextInput id='first_name'
|
||||
label="Имя:"
|
||||
value={first_name}
|
||||
onChange={event => setFirstName(event.target.value)}
|
||||
/>
|
||||
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => setLastName(event.target.value)}/>
|
||||
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => setEmail(event.target.value)}/>
|
||||
</div>
|
||||
<div className='flex justify-center px-0 py-4'>
|
||||
<button
|
||||
type='submit'
|
||||
className={`px-2 py-1 border clr-btn-green`}
|
||||
|
||||
disabled={processing}>
|
||||
<span>Сохранить мои данные</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ChangePassword />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
|
@ -1,16 +1,60 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { Loader } from '../../components/Common/Loader';
|
||||
import MiniButton from '../../components/Common/MiniButton';
|
||||
import { EyeIcon, EyeOffIcon } from '../../components/Icons';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useLibrary } from '../../context/LibraryContext';
|
||||
import { useUserProfile } from '../../context/UserProfileContext';
|
||||
import { UserProfile } from './UserProfile';
|
||||
import EditorPassword from './EditorPassword';
|
||||
import EditorProfile from './EditorProfile';
|
||||
import ViewSubscriptions from './ViewSubscriptions';
|
||||
|
||||
function UserTabs() {
|
||||
const { user, error, loading } = useUserProfile();
|
||||
const { user: auth } = useAuth();
|
||||
const { items } = useLibrary();
|
||||
|
||||
const [showSubs, setShowSubs] = useState(true);
|
||||
|
||||
const subscriptions = useMemo(
|
||||
() => {
|
||||
return items.filter(item => auth?.subscriptions.includes(item.id));
|
||||
}, [auth, items]);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{ loading && <Loader /> }
|
||||
{ error && <BackendError error={error} />}
|
||||
{ user && <UserProfile /> }
|
||||
{ user &&
|
||||
<div className='flex justify-center gap-2 py-2'>
|
||||
<div className='flex flex-col gap-2 min-w-max'>
|
||||
<div className='relative w-full'>
|
||||
<div className='absolute top-0 right-0 mt-2'>
|
||||
<MiniButton
|
||||
tooltip='Показать/Скрыть список отслеживаний'
|
||||
icon={showSubs
|
||||
? <EyeIcon color='text-primary' size={5}/>
|
||||
: <EyeOffIcon color='text-primary' size={5}/>
|
||||
}
|
||||
onClick={() => setShowSubs(prev => !prev)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Учетные данные пользователя</h1>
|
||||
<div className='flex justify-center py-2 max-w-fit'>
|
||||
<EditorProfile />
|
||||
<EditorPassword />
|
||||
</div>
|
||||
</div>
|
||||
{subscriptions.length > 0 && showSubs &&
|
||||
<div className='flex flex-col w-full gap-2 pl-4'>
|
||||
<h1>Отслеживаемые схемы</h1>
|
||||
<ViewSubscriptions items={subscriptions} />
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ConceptDataTable from '../../components/Common/ConceptDataTable';
|
||||
import { ILibraryItem } from '../../utils/models';
|
||||
|
||||
interface ViewSubscriptionsProps {
|
||||
items: ILibraryItem[]
|
||||
}
|
||||
|
||||
function ViewSubscriptions({items}: ViewSubscriptionsProps) {
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
|
||||
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`);
|
||||
|
||||
const columns = useMemo(() =>
|
||||
[
|
||||
{
|
||||
name: 'Шифр',
|
||||
id: 'alias',
|
||||
maxWidth: '140px',
|
||||
selector: (item: ILibraryItem) => item.alias,
|
||||
sortable: true,
|
||||
reorder: true
|
||||
},
|
||||
{
|
||||
name: 'Название',
|
||||
id: 'title',
|
||||
minWidth: '50%',
|
||||
selector: (item: ILibraryItem) => item.title,
|
||||
sortable: true,
|
||||
reorder: true
|
||||
},
|
||||
{
|
||||
name: 'Обновлена',
|
||||
id: 'time_update',
|
||||
selector: (item: ILibraryItem) => item.time_update,
|
||||
format: (item: ILibraryItem) => new Date(item.time_update).toLocaleString(intl.locale),
|
||||
sortable: true,
|
||||
reorder: true
|
||||
}
|
||||
], [intl]);
|
||||
|
||||
return (
|
||||
<ConceptDataTable
|
||||
className='h-full overflow-auto border clr-border'
|
||||
columns={columns}
|
||||
data={items}
|
||||
defaultSortFieldId='time_update'
|
||||
defaultSortAsc={false}
|
||||
|
||||
noDataComponent={
|
||||
<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>
|
||||
}
|
||||
|
||||
striped
|
||||
dense
|
||||
highlightOnHover
|
||||
pointerOnHover
|
||||
onRowClicked={openRSForm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewSubscriptions;
|
|
@ -118,7 +118,7 @@ export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
|
|||
export function getLibrary(request: FrontPull<ILibraryItem[]>) {
|
||||
AxiosGet({
|
||||
title: 'Available RSForms (Library) list',
|
||||
endpoint: '/api/library/active/',
|
||||
endpoint: '/api/library/active',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ export function getLibrary(request: FrontPull<ILibraryItem[]>) {
|
|||
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibraryItem>) {
|
||||
AxiosPost({
|
||||
title: 'New RSForm',
|
||||
endpoint: '/api/rsforms/create-detailed/',
|
||||
endpoint: '/api/rsforms/create-detailed',
|
||||
request: request,
|
||||
options: {
|
||||
headers: {
|
||||
|
@ -139,7 +139,7 @@ export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibrary
|
|||
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCreateData, IRSFormData>) {
|
||||
AxiosPost({
|
||||
title: 'clone RSForm',
|
||||
endpoint: `/api/library/${target}/clone/`,
|
||||
endpoint: `/api/library/${target}/clone`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ export function postCloneLibraryItem(target: string, request: FrontExchange<IRSF
|
|||
export function getRSFormDetails(target: string, request: FrontPull<IRSFormData>) {
|
||||
AxiosGet({
|
||||
title: `RSForm details for id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/details/`,
|
||||
endpoint: `/api/rsforms/${target}/details`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ export function getRSFormDetails(target: string, request: FrontPull<IRSFormData>
|
|||
export function patchLibraryItem(target: string, request: FrontExchange<ILibraryUpdateData, ILibraryItem>) {
|
||||
AxiosPatch({
|
||||
title: `RSForm id=${target}`,
|
||||
endpoint: `/api/library/${target}/`,
|
||||
endpoint: `/api/library/${target}`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ export function patchLibraryItem(target: string, request: FrontExchange<ILibrary
|
|||
export function deleteLibraryItem(target: string, request: FrontAction) {
|
||||
AxiosDelete({
|
||||
title: `RSForm id=${target}`,
|
||||
endpoint: `/api/library/${target}/`,
|
||||
endpoint: `/api/library/${target}`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -171,7 +171,23 @@ export function deleteLibraryItem(target: string, request: FrontAction) {
|
|||
export function postClaimLibraryItem(target: string, request: FrontPull<ILibraryItem>) {
|
||||
AxiosPost({
|
||||
title: `Claim on RSForm id=${target}`,
|
||||
endpoint: `/api/library/${target}/claim/`,
|
||||
endpoint: `/api/library/${target}/claim`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postSubscribe(target: string, request: FrontAction) {
|
||||
AxiosPost({
|
||||
title: `Claim on RSForm id=${target}`,
|
||||
endpoint: `/api/library/${target}/subscribe`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteUnsubscribe(target: string, request: FrontAction) {
|
||||
AxiosDelete({
|
||||
title: `Claim on RSForm id=${target}`,
|
||||
endpoint: `/api/library/${target}/unsubscribe`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -179,7 +195,7 @@ export function postClaimLibraryItem(target: string, request: FrontPull<ILibrary
|
|||
export function getTRSFile(target: string, request: FrontPull<Blob>) {
|
||||
AxiosGet({
|
||||
title: `RSForm TRS file for id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/export-trs/`,
|
||||
endpoint: `/api/rsforms/${target}/export-trs`,
|
||||
request: request,
|
||||
options: { responseType: 'blob' }
|
||||
});
|
||||
|
@ -188,7 +204,7 @@ export function getTRSFile(target: string, request: FrontPull<Blob>) {
|
|||
export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
|
||||
AxiosPost({
|
||||
title: `New Constituenta for RSForm id=${schema}: ${request.data.alias}`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-create/`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-create`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -196,7 +212,7 @@ export function postNewConstituenta(schema: string, request: FrontExchange<ICstC
|
|||
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Delete Constituents for RSForm id=${schema}: ${request.data.items.map(item => String(item.id)).join(' ')}`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-multidelete/`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-multidelete`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -204,7 +220,7 @@ export function patchDeleteConstituenta(schema: string, request: FrontExchange<I
|
|||
export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
|
||||
AxiosPatch({
|
||||
title: `Constituenta id=${target}`,
|
||||
endpoint: `/api/constituents/${target}/`,
|
||||
endpoint: `/api/constituents/${target}`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -212,7 +228,7 @@ export function patchConstituenta(target: string, request: FrontExchange<ICstUpd
|
|||
export function patchRenameConstituenta(schema: string, request: FrontExchange<ICstRenameData, ICstCreatedResponse>) {
|
||||
AxiosPatch({
|
||||
title: `Renaming constituenta id=${request.data.id} for schema id=${schema}`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-rename/`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-rename`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -220,7 +236,7 @@ export function patchRenameConstituenta(schema: string, request: FrontExchange<I
|
|||
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${request.data.move_to}`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-moveto/`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-moveto`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -228,7 +244,7 @@ export function patchMoveConstituenta(schema: string, request: FrontExchange<ICs
|
|||
export function postCheckExpression(schema: string, request: FrontExchange<IRSExpression, IExpressionParse>) {
|
||||
AxiosPost({
|
||||
title: `Check expression for RSForm id=${schema}: ${request.data.expression }`,
|
||||
endpoint: `/api/rsforms/${schema}/check/`,
|
||||
endpoint: `/api/rsforms/${schema}/check`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -236,7 +252,7 @@ export function postCheckExpression(schema: string, request: FrontExchange<IRSEx
|
|||
export function postResolveText(schema: string, request: FrontExchange<IRefsText, IReferenceData>) {
|
||||
AxiosPost({
|
||||
title: `Resolve text references for RSForm id=${schema}: ${request.data.text }`,
|
||||
endpoint: `/api/rsforms/${schema}/resolve/`,
|
||||
endpoint: `/api/rsforms/${schema}/resolve`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -244,7 +260,7 @@ export function postResolveText(schema: string, request: FrontExchange<IRefsText
|
|||
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Reset alias for RSForm id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/reset-aliases/`,
|
||||
endpoint: `/api/rsforms/${target}/reset-aliases`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -252,7 +268,7 @@ export function patchResetAliases(target: string, request: FrontPull<IRSFormData
|
|||
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Replacing data with trs file for RSForm id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/load-trs/`,
|
||||
endpoint: `/api/rsforms/${target}/load-trs`,
|
||||
request: request,
|
||||
options: {
|
||||
headers: {
|
||||
|
|
|
@ -34,4 +34,5 @@ export const prefixes = {
|
|||
cst_list: 'cst-list-',
|
||||
cst_status_list: 'cst-status-list-',
|
||||
topic_list: 'topic-list-',
|
||||
library_list: 'library-list-'
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ export interface IUser {
|
|||
last_name: string
|
||||
}
|
||||
|
||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {}
|
||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
||||
subscriptions: number[]
|
||||
}
|
||||
|
||||
export interface IUserLoginData extends Pick<IUser, 'username'> {
|
||||
password: string
|
||||
|
@ -261,6 +263,7 @@ extends ILibraryItem {
|
|||
items: IConstituenta[]
|
||||
stats: IRSFormStats
|
||||
graph: Graph
|
||||
subscribers: number[]
|
||||
}
|
||||
|
||||
export interface IRSFormData extends Omit<IRSForm, 'stats' | 'graph'> {}
|
||||
|
|
Loading…
Reference in New Issue
Block a user