From 93d56ef4fa3186b075eb4d08f0d6e9473d1245eb Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 3 Jun 2024 01:17:27 +0300 Subject: [PATCH] Fix Editor filter and view permissions --- rsconcept/backend/apps/rsform/permissions.py | 12 +++++++++++- rsconcept/backend/apps/rsform/views/rsforms.py | 4 +++- rsconcept/backend/apps/users/serializers.py | 8 +++++--- rsconcept/backend/apps/users/tests/t_views.py | 2 ++ rsconcept/backend/apps/users/views.py | 6 ++++-- rsconcept/backend/project/settings.py | 2 +- rsconcept/frontend/src/context/LibraryContext.tsx | 2 +- rsconcept/frontend/src/models/user.ts | 1 + rsconcept/frontend/src/utils/labels.ts | 4 ++-- 9 files changed, 30 insertions(+), 11 deletions(-) diff --git a/rsconcept/backend/apps/rsform/permissions.py b/rsconcept/backend/apps/rsform/permissions.py index ee51a795..9c9a4a81 100644 --- a/rsconcept/backend/apps/rsform/permissions.py +++ b/rsconcept/backend/apps/rsform/permissions.py @@ -1,5 +1,5 @@ ''' Custom Permission classes. - Hierarchy: Anonymous -> User -> Editor -> Owner -> Admin + Hierarchy: Anyone -> User -> Editor -> Owner -> Admin ''' from typing import Any, cast @@ -66,6 +66,16 @@ class ItemEditor(ItemOwner): return super().has_object_permission(request, view, obj) +class ItemAnyone(ItemEditor): + ''' Item permission: Anyone if public. ''' + + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + item = _extract_item(obj) + if item.access_policy == m.AccessPolicy.PUBLIC: + return True + return super().has_object_permission(request, view, obj) + + class EditorMixin(APIView): ''' Editor permissions mixin for API views. ''' diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index e1cbc57f..5a3213f0 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -36,7 +36,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr 'load_trs', 'cst_create', 'cst_delete_multiple', 'reset_aliases', 'cst_rename', 'cst_substitute' ]: - permission_list = [permissions.ItemOwner] + permission_list = [permissions.ItemEditor] + elif self.action in ['contents', 'details', 'export_trs', 'resolve', 'check']: + permission_list = [permissions.ItemAnyone] else: permission_list = [permissions.Anyone] return [permission() for permission in permission_list] diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 2843f832..d7ed3dd6 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -3,7 +3,7 @@ from django.contrib.auth import authenticate from django.contrib.auth.password_validation import validate_password from rest_framework import serializers -from apps.rsform.models import Subscription +from apps.rsform.models import Editor, Subscription from . import messages as msg from . import models @@ -69,14 +69,16 @@ class AuthSerializer(serializers.Serializer): 'id': None, 'username': '', 'is_staff': False, - 'subscriptions': [] + 'subscriptions': [], + 'editor': [] } 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)] + 'subscriptions': [sub.item.pk for sub in Subscription.objects.filter(user=instance)], + 'editor': [edit.item.pk for edit in Editor.objects.filter(editor=instance)] } diff --git a/rsconcept/backend/apps/users/tests/t_views.py b/rsconcept/backend/apps/users/tests/t_views.py index 6b7d40de..44551a5c 100644 --- a/rsconcept/backend/apps/users/tests/t_views.py +++ b/rsconcept/backend/apps/users/tests/t_views.py @@ -44,6 +44,7 @@ class TestUserAPIViews(EndpointTester): self.assertEqual(response.data['username'], self.user.username) self.assertEqual(response.data['is_staff'], self.user.is_staff) self.assertEqual(response.data['subscriptions'], []) + self.assertEqual(response.data['editor'], []) self.logout() response = self.executeOK() @@ -51,6 +52,7 @@ class TestUserAPIViews(EndpointTester): self.assertEqual(response.data['username'], '') self.assertEqual(response.data['is_staff'], False) self.assertEqual(response.data['subscriptions'], []) + self.assertEqual(response.data['editor'], []) class TestUserUserProfileAPIView(EndpointTester): diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py index b41ea6cc..ef67bb72 100644 --- a/rsconcept/backend/apps/users/views.py +++ b/rsconcept/backend/apps/users/views.py @@ -112,8 +112,10 @@ class UpdatePassword(views.APIView): if serializer.is_valid(): old_password = serializer.data.get("old_password") if not self.object.check_password(old_password): - return Response({"old_password": ["Wrong password."]}, - status=c.HTTP_400_BAD_REQUEST) + return Response( + {"old_password": ["Wrong password."]}, + status=c.HTTP_400_BAD_REQUEST + ) # Note: set_password also hashes the password that the user will get self.object.set_password(serializer.data.get("new_password")) self.object.save() diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index 7abd0854..2d2f0de2 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -172,7 +172,7 @@ DATABASES = { SPECTACULAR_SETTINGS = { 'TITLE': 'ConceptPortal API', 'DESCRIPTION': 'Портал для работы с экспликациями концептуальных схем', - 'VERSION': '0.1.1', + 'VERSION': '0.1.2', 'SERVE_INCLUDE_SCHEMA': False, 'COMPONENT_SPLIT_PATCH': True, diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx index b88de7a2..56e42a75 100644 --- a/rsconcept/frontend/src/context/LibraryContext.tsx +++ b/rsconcept/frontend/src/context/LibraryContext.tsx @@ -81,7 +81,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => { result = result.filter(item => filter.isSubscribed == user?.subscriptions.includes(item.id)); } if (filter.isEditor !== undefined) { - // TODO: load editors from backend + result = result.filter(item => filter.isEditor == user?.editor.includes(item.id)); } if (filter.query) { result = result.filter(item => matchLibraryItem(item, filter.query!)); diff --git a/rsconcept/frontend/src/models/user.ts b/rsconcept/frontend/src/models/user.ts index 17b1c0c8..485f970b 100644 --- a/rsconcept/frontend/src/models/user.ts +++ b/rsconcept/frontend/src/models/user.ts @@ -27,6 +27,7 @@ export interface IUser { */ export interface ICurrentUser extends Pick { subscriptions: LibraryItemID[]; + editor: LibraryItemID[]; } /** diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index 03005f96..954afcb9 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -262,7 +262,7 @@ export function labelLocationHead(head: LocationHead): string { switch (head) { case LocationHead.USER: return 'личные (/U)'; case LocationHead.COMMON: return 'общие (/S)'; - case LocationHead.LIBRARY: return 'неизменные (/L)'; + case LocationHead.LIBRARY: return 'примеры (/L)'; case LocationHead.PROJECTS: return 'проекты (/P)'; } } @@ -275,7 +275,7 @@ export function describeLocationHead(head: LocationHead): string { switch (head) { case LocationHead.USER: return 'Личные схемы пользователя'; case LocationHead.COMMON: return 'Рабочий каталог публичных схем'; - case LocationHead.LIBRARY: return 'Каталог неизменных схем'; + case LocationHead.LIBRARY: return 'Каталог неизменных схем-примеров'; case LocationHead.PROJECTS: return 'Рабочий каталог проектных схем'; } }