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