diff --git a/make/commands.py b/make/commands.py index 3e34be355..a190688c3 100644 --- a/make/commands.py +++ b/make/commands.py @@ -12,7 +12,9 @@ def check(args=None): """ Checks for pep8 and other code styling conventions. """ - return call('flake8 --max-line-length=150 --statistics openslides tests') + value = call('flake8 --max-line-length=150 --statistics openslides tests') + value += call('python -m mypy openslides/ tests/') + return value @command('travis', help='Runs the code that travis does') diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index 8b602f935..dee6ffbcc 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -10,17 +10,8 @@ class ItemAccessPermissions(BaseAccessPermissions): """ base_permission = 'agenda.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import ItemSerializer - - return ItemSerializer - # TODO: In the following method we use full_data['is_hidden'] and # full_data['is_internal'] but this can be out of date. - async def get_restricted_data( self, full_data: List[Dict[str, Any]], diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py index 3629d1407..5f2fe966d 100644 --- a/openslides/agenda/apps.py +++ b/openslides/agenda/apps.py @@ -22,6 +22,7 @@ class AgendaAppConfig(AppConfig): listen_to_related_object_post_delete, listen_to_related_object_post_save) from .views import ItemViewSet + from . import serializers # noqa from ..utils.access_permissions import required_user # Define projector elements. diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py index 05814a25e..7cadf883c 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -1,7 +1,7 @@ from typing import Any, Dict, List from ..utils.access_permissions import BaseAccessPermissions -from ..utils.auth import async_has_perm, has_perm +from ..utils.auth import async_has_perm class AssignmentAccessPermissions(BaseAccessPermissions): @@ -10,18 +10,6 @@ class AssignmentAccessPermissions(BaseAccessPermissions): """ base_permission = 'assignments.can_see' - def get_serializer_class(self, user=None): - """ - Returns different serializer classes according to users permissions. - """ - from .serializers import AssignmentFullSerializer, AssignmentShortSerializer - - if user is None or (has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage')): - serializer_class = AssignmentFullSerializer - else: - serializer_class = AssignmentShortSerializer - return serializer_class - async def get_restricted_data( self, full_data: List[Dict[str, Any]], diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index 555faf16c..8db2e9138 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -15,11 +15,12 @@ class AssignmentsAppConfig(AppConfig): def ready(self): # Import all required stuff. from ..core.signals import permission_change + from ..utils.access_permissions import required_user from ..utils.rest_api import router + from . import serializers # noqa from .projector import get_projector_elements from .signals import get_permission_change_data from .views import AssignmentViewSet, AssignmentPollViewSet - from ..utils.access_permissions import required_user # Define projector elements. register_projector_elements(get_projector_elements()) diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 438fd46f8..6b2cb1b2b 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -238,31 +238,3 @@ class AssignmentFullSerializer(ModelSerializer): assignment.agenda_item_update_information['parent_id'] = agenda_parent_id assignment.save() return assignment - - -class AssignmentShortSerializer(AssignmentFullSerializer): - """ - Serializer for assignment.models.Assignment objects. Without unpublished poll. - """ - assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True) - polls = AssignmentShortPollSerializer(many=True, read_only=True) - - class Meta: - model = Assignment - fields = ( - 'id', - 'title', - 'description', - 'open_posts', - 'phase', - 'assignment_related_users', - 'poll_description_default', - 'polls', - 'agenda_item_id', - 'tags',) - validators = (posts_validator,) - - def validate(self, data): - if 'description' in data: - data['description'] = validate_html(data['description']) - return data diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index 1a8ce04f5..b004f7199 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -8,28 +8,12 @@ class ProjectorAccessPermissions(BaseAccessPermissions): """ base_permission = 'core.can_see_projector' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import ProjectorSerializer - - return ProjectorSerializer - class TagAccessPermissions(BaseAccessPermissions): """ Access permissions container for Tag and TagViewSet. """ - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import TagSerializer - - return TagSerializer - class ChatMessageAccessPermissions(BaseAccessPermissions): """ @@ -37,14 +21,6 @@ class ChatMessageAccessPermissions(BaseAccessPermissions): """ base_permission = 'core.can_use_chat' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import ChatMessageSerializer - - return ChatMessageSerializer - class ProjectorMessageAccessPermissions(BaseAccessPermissions): """ @@ -52,14 +28,6 @@ class ProjectorMessageAccessPermissions(BaseAccessPermissions): """ base_permission = 'core.can_see_projector' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import ProjectorMessageSerializer - - return ProjectorMessageSerializer - class CountdownAccessPermissions(BaseAccessPermissions): """ @@ -67,14 +35,6 @@ class CountdownAccessPermissions(BaseAccessPermissions): """ base_permission = 'core.can_see_projector' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import CountdownSerializer - - return CountdownSerializer - class ConfigAccessPermissions(BaseAccessPermissions): """ @@ -82,14 +42,6 @@ class ConfigAccessPermissions(BaseAccessPermissions): ConfigViewSet). """ - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import ConfigSerializer - - return ConfigSerializer - class HistoryAccessPermissions(BaseAccessPermissions): """ @@ -102,11 +54,3 @@ class HistoryAccessPermissions(BaseAccessPermissions): model instances. """ return await async_in_some_groups(user_id, [GROUP_ADMIN_PK]) - - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import HistorySerializer - - return HistorySerializer diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 5fc3ac3e3..86958c32d 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -22,6 +22,7 @@ class CoreAppConfig(AppConfig): from ..utils.rest_api import router from ..utils.cache import element_cache from .projector import get_projector_elements + from . import serializers # noqa from .signals import ( delete_django_app_permissions, get_permission_change_data, diff --git a/openslides/mediafiles/access_permissions.py b/openslides/mediafiles/access_permissions.py index 2b438e1de..345bc6a72 100644 --- a/openslides/mediafiles/access_permissions.py +++ b/openslides/mediafiles/access_permissions.py @@ -10,14 +10,6 @@ class MediafileAccessPermissions(BaseAccessPermissions): """ base_permission = 'mediafiles.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import MediafileSerializer - - return MediafileSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], diff --git a/openslides/mediafiles/apps.py b/openslides/mediafiles/apps.py index a7c340310..74c8dcfdc 100644 --- a/openslides/mediafiles/apps.py +++ b/openslides/mediafiles/apps.py @@ -18,6 +18,7 @@ class MediafilesAppConfig(AppConfig): from .projector import get_projector_elements from .signals import get_permission_change_data from .views import MediafileViewSet + from . import serializers # noqa from ..utils.access_permissions import required_user # Define projector elements. diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index f3603778d..ce6f2cbb5 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -11,14 +11,6 @@ class MotionAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import MotionSerializer - - return MotionSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], @@ -71,14 +63,6 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import MotionChangeRecommendationSerializer - - return MotionChangeRecommendationSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], @@ -107,14 +91,6 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import MotionCommentSectionSerializer - - return MotionCommentSectionSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], @@ -140,14 +116,6 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import StatuteParagraphSerializer - - return StatuteParagraphSerializer - class CategoryAccessPermissions(BaseAccessPermissions): """ @@ -155,14 +123,6 @@ class CategoryAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import CategorySerializer - - return CategorySerializer - class MotionBlockAccessPermissions(BaseAccessPermissions): """ @@ -170,25 +130,9 @@ class MotionBlockAccessPermissions(BaseAccessPermissions): """ base_permission = 'motions.can_see' - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import MotionBlockSerializer - - return MotionBlockSerializer - class WorkflowAccessPermissions(BaseAccessPermissions): """ Access permissions container for Workflow and WorkflowViewSet. """ base_permission = 'motions.can_see' - - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import WorkflowSerializer - - return WorkflowSerializer diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 7ce32bdeb..553cd7411 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -21,6 +21,7 @@ class MotionsAppConfig(AppConfig): create_builtin_workflows, get_permission_change_data, ) + from . import serializers # noqa from .views import ( CategoryViewSet, StatuteParagraphViewSet, diff --git a/openslides/topics/access_permissions.py b/openslides/topics/access_permissions.py index 49a254a73..b0a89a198 100644 --- a/openslides/topics/access_permissions.py +++ b/openslides/topics/access_permissions.py @@ -6,11 +6,3 @@ class TopicAccessPermissions(BaseAccessPermissions): Access permissions container for Topic and TopicViewSet. """ base_permission = 'agenda.can_see' - - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import TopicSerializer - - return TopicSerializer diff --git a/openslides/topics/apps.py b/openslides/topics/apps.py index edc9f572e..5e89c4051 100644 --- a/openslides/topics/apps.py +++ b/openslides/topics/apps.py @@ -16,6 +16,7 @@ class TopicsAppConfig(AppConfig): from .projector import get_projector_elements from .signals import get_permission_change_data from .views import TopicViewSet + from . import serializers # noqa # Define projector elements. register_projector_elements(get_projector_elements()) diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py index a8b6297e3..d85e065fd 100644 --- a/openslides/users/access_permissions.py +++ b/openslides/users/access_permissions.py @@ -10,14 +10,6 @@ class UserAccessPermissions(BaseAccessPermissions): Access permissions container for User and UserViewSet. """ - def get_serializer_class(self, user=None): - """ - Returns different serializer classes with respect user's permissions. - """ - from .serializers import UserFullSerializer - - return UserFullSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], @@ -95,14 +87,6 @@ class GroupAccessPermissions(BaseAccessPermissions): Access permissions container for Groups. Everyone can see them """ - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import GroupSerializer - - return GroupSerializer - class PersonalNoteAccessPermissions(BaseAccessPermissions): """ @@ -110,14 +94,6 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions): can handle personal notes. """ - def get_serializer_class(self, user=None): - """ - Returns serializer class. - """ - from .serializers import PersonalNoteSerializer - - return PersonalNoteSerializer - async def get_restricted_data( self, full_data: List[Dict[str, Any]], diff --git a/openslides/utils/access_permissions.py b/openslides/utils/access_permissions.py index 840315315..eb49737f5 100644 --- a/openslides/utils/access_permissions.py +++ b/openslides/utils/access_permissions.py @@ -1,8 +1,6 @@ from typing import Any, Callable, Dict, List, Set from asgiref.sync import async_to_sync -from django.db.models import Model -from rest_framework.serializers import Serializer from .auth import async_anonymous_is_enabled, async_has_perm, user_to_user_id from .cache import element_cache @@ -41,25 +39,6 @@ class BaseAccessPermissions: else: return bool(user_id) or await async_anonymous_is_enabled() - def get_serializer_class(self, user_id: int = 0) -> Serializer: - """ - Returns different serializer classes according to users permissions. - - This should return the serializer for full data access if user is - None. See get_full_data(). - """ - # TODO: Rewrite me by using an serializer_class attribute and removing - # the user_id argument. - raise NotImplementedError( - "You have to add the method 'get_serializer_class' to your " - "access permissions class.".format(self)) - - def get_full_data(self, instance: Model) -> Dict[str, Any]: - """ - Returns all possible serialized data for the given instance. - """ - return self.get_serializer_class()(instance).data - async def get_restricted_data( self, full_data: List[Dict[str, Any]], user_id: int) -> List[Dict[str, Any]]: @@ -71,11 +50,6 @@ class BaseAccessPermissions: the return is the same. Returns an empty list if the user has no read access. Returns reduced data if the user has limited access. Default: Returns full data if the user has read access to model instances. - - Hint: You should override this method if your get_serializer_class() - method returns different serializers for different users or if you - have access restrictions in your view or viewset in methods like - retrieve() or list(). """ return full_data if await self.async_check_permissions(user_id) else [] diff --git a/openslides/utils/models.py b/openslides/utils/models.py index d8f556e34..4b863e497 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -4,6 +4,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from .access_permissions import BaseAccessPermissions +from .rest_api import model_serializer_classes from .utils import convert_camel_case_to_pseudo_snake_case @@ -134,4 +135,5 @@ class RESTModelMixin: """ Returns the full_data of the instance. """ - return self.get_access_permissions().get_full_data(self) + serializer_class = model_serializer_classes[type(self)] + return serializer_class(self).data diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index ffb6e3b7f..7f2d368f4 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -2,6 +2,7 @@ from collections import OrderedDict from typing import Any, Dict, Iterable, Optional, Type from asgiref.sync import async_to_sync +from django.db.models import Model from django.http import Http404 from rest_framework import status from rest_framework.decorators import detail_route, list_route @@ -32,6 +33,7 @@ from rest_framework.serializers import ( PrimaryKeyRelatedField, RelatedField, Serializer, + SerializerMetaclass, SerializerMethodField, ValidationError, ) @@ -152,17 +154,44 @@ class PermissionMixin: def get_serializer_class(self) -> Type[Serializer]: """ - Overridden method to return the serializer class given by the - access permissions container. + Overridden method to return the serializer class for the model. """ - if self.get_access_permissions() is not None: - serializer_class = self.get_access_permissions().get_serializer_class(self.request.user) # type: ignore + model = self.get_queryset().model # type: ignore + try: + return model_serializer_classes[model] + except AttributeError: + # If there is no known serializer class for the model, return the + # default serializer class. + return super().get_serializer_class() # type: ignore + + +model_serializer_classes: Dict[Type[Model], Serializer] = {} + + +class ModelSerializerRegisterer(SerializerMetaclass): + """ + Meta class for model serializer that detects the corresponding model + and saves it. + """ + + def __new__(cls, name, bases, attrs): # type: ignore + """ + Detects the corresponding model from the ModelSerializer by + looking into the Meta-class. + + Does nothing, if the Meta-class does not have the model attribute. + """ + serializer_class = super().__new__(cls, name, bases, attrs) + try: + model = serializer_class.Meta.model + except AttributeError: + pass else: - serializer_class = super().get_serializer_class() # type: ignore + model_serializer_classes[model] = serializer_class return serializer_class -class ModelSerializer(_ModelSerializer): +class ModelSerializer(_ModelSerializer, metaclass=ModelSerializerRegisterer): """ ModelSerializer that changes the field names of related fields to FIELD_NAME_id.