From 8cee4d24242800e28364232d3f65f859a42d25f7 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:25:29 +0800 Subject: [PATCH 01/15] feat: add parent permission check. --- rest_framework_extensions/mixins.py | 54 ++++++++++++++++++++++++++++ rest_framework_extensions/routers.py | 1 + 2 files changed, 55 insertions(+) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 8a56c5f..765b3ca 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -1,8 +1,21 @@ from rest_framework_extensions.cache.mixins import CacheResponseMixin +from django.core.exceptions import ValidationError # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings from django.http import Http404 +from django.shortcuts import get_object_or_404 as _get_object_or_404 + + +def get_object_or_404(queryset, *filter_args, **filter_kwargs): + """ + Same as Django's standard shortcut, but make sure to also raise 404 + if the filter_kwargs don't match the required types. + """ + try: + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) + except (TypeError, ValueError, ValidationError): + raise Http404 class DetailSerializerMixin: @@ -50,6 +63,47 @@ def get_page_size(self, request): class NestedViewSetMixin: + parent_viewsets = set() + + def check_parent_object_permissions(self, request): + # if parent viewset haven't init yet, then will raise no "kwargs" attribute error, but it doesn't matter, just ignore + try: + parents_query_dict = self.get_parents_query_dict() + except: + return + if not parents_query_dict: + return + current_model = self.get_queryset().model + # TODO + # 1. for model__submodel case. + # 2. for generic relations case. + for parent_model_key, parent_model_filter_value in reversed(parents_query_dict.items()): + parent_model = current_model._meta.get_field( + parent_model_key).related_model + for parent_viewset_class in self.parent_viewsets: + parent_viewset = parent_viewset_class() + parent_viewset_model = getattr( + parent_viewset, "model", None) or parent_viewset.queryset.model + if parent_viewset_model == parent_model: + parent_obj = get_object_or_404( + parent_viewset_model.objects.all(), + **{parent_viewset.lookup_field: parent_model_filter_value} + ) + parent_viewset.check_object_permissions( + request, parent_obj + ) + current_model = parent_model + + def check_permissions(self, request): + super().check_permissions(request) + if self.parent_viewsets: + self.check_parent_object_permissions(request) + + def check_object_permissions(self, request, obj): + super().check_object_permissions(request, obj) + if self.parent_viewsets: + self.check_parent_object_permissions(request) + def get_queryset(self): return self.filter_queryset_by_parents_lookups( super().get_queryset() diff --git a/rest_framework_extensions/routers.py b/rest_framework_extensions/routers.py index 2f119f8..df5bd45 100644 --- a/rest_framework_extensions/routers.py +++ b/rest_framework_extensions/routers.py @@ -17,6 +17,7 @@ def register(self, prefix, viewset, basename, parents_query_lookups): viewset=viewset, basename=basename, ) + viewset.parent_viewsets.add(self.parent_viewset) return NestedRegistryItem( router=self.router, parent_prefix=prefix, From 2f8a9dcf1a72d333d8be6942cdcfbce69e5d3817 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:29:10 +0800 Subject: [PATCH 02/15] Update mixins.py --- rest_framework_extensions/mixins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 765b3ca..f6d1138 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -77,9 +77,9 @@ def check_parent_object_permissions(self, request): # TODO # 1. for model__submodel case. # 2. for generic relations case. - for parent_model_key, parent_model_filter_value in reversed(parents_query_dict.items()): + for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): parent_model = current_model._meta.get_field( - parent_model_key).related_model + parent_model_lookup_name).related_model for parent_viewset_class in self.parent_viewsets: parent_viewset = parent_viewset_class() parent_viewset_model = getattr( @@ -87,7 +87,7 @@ def check_parent_object_permissions(self, request): if parent_viewset_model == parent_model: parent_obj = get_object_or_404( parent_viewset_model.objects.all(), - **{parent_viewset.lookup_field: parent_model_filter_value} + **{parent_viewset.lookup_field: parent_model_lookup_value} ) parent_viewset.check_object_permissions( request, parent_obj From 16751da0a181380b5cc2dcaabb3d31ed4b3039e0 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 18:34:33 +0800 Subject: [PATCH 03/15] feat: add ownership check when create and update --- rest_framework_extensions/mixins.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index f6d1138..5f6d8ad 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -3,6 +3,7 @@ # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings +from rest_framework import status, exceptions from django.http import Http404 from django.shortcuts import get_object_or_404 as _get_object_or_404 @@ -65,6 +66,29 @@ def get_page_size(self, request): class NestedViewSetMixin: parent_viewsets = set() + def check_ownership(self, serializer): + parent_query_dicts = self.get_parents_query_dict() + if parent_query_dicts: + parent_name, parent_value = list(parent_query_dicts.items())[-1] + items = serializer.validated_data + if not isinstance(items, list): + items = [items] + for item in items: + if item.get(parent_name, None) is None: + raise exceptions.PermissionDenied( + detail=f"You must specific '{parent_name}'", code=status.HTTP_403_FORBIDDEN) + if item.get(parent_name, None) != parent_value: + raise exceptions.PermissionDenied( + detail=f"You don't have permission to operate item that belone to '{parent_name}:{parent_value}'", code=status.HTTP_403_FORBIDDEN) + + def perform_create(self, serializer): + self.check_ownership(serializer) + super().perform_create(serializer) + + def perform_update(self, serializer): + self.check_ownership(serializer) + super().perform_update(serializer) + def check_parent_object_permissions(self, request): # if parent viewset haven't init yet, then will raise no "kwargs" attribute error, but it doesn't matter, just ignore try: From 2e85ed5659b56adac992db3c07b7bc9fce315317 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 18:41:39 +0800 Subject: [PATCH 04/15] fix: import get_object_or_404 from rest_framework --- rest_framework_extensions/mixins.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 5f6d8ad..010d6f2 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -1,22 +1,11 @@ from rest_framework_extensions.cache.mixins import CacheResponseMixin from django.core.exceptions import ValidationError # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin +from django.http import Http404 from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings from rest_framework import status, exceptions -from django.http import Http404 -from django.shortcuts import get_object_or_404 as _get_object_or_404 - - -def get_object_or_404(queryset, *filter_args, **filter_kwargs): - """ - Same as Django's standard shortcut, but make sure to also raise 404 - if the filter_kwargs don't match the required types. - """ - try: - return _get_object_or_404(queryset, *filter_args, **filter_kwargs) - except (TypeError, ValueError, ValidationError): - raise Http404 +from rest_framework.generics import get_object_or_404 class DetailSerializerMixin: From 23e57f2b594dbeb7f125db0fcd629eae1986bbd5 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 20:07:42 +0800 Subject: [PATCH 05/15] feat: add bulk create and multiple serializer mixin --- rest_framework_extensions/mixins.py | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 010d6f2..3a62fc7 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -52,6 +52,40 @@ def get_page_size(self, request): # pass +class BulkCreateModelMixin: + """ + Builk create model instance. + Just post data like: + [ + {"name": "xxx"}, + {"name": "xxx2"}, + ] + """ + + def get_serializer(self, *args, **kwargs): + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + s = super().get_serializer(*args, **kwargs) + return s + + +class MultiSerializerViewSetMixin: + """ + serializer_action_classes = { + list: ListSerializer, + : Serializer, + ... + } + """ + serializer_action_classes = {} + def get_serializer_class(self): + try: + return self.serializer_action_classes[self.action] + except (KeyError, AttributeError): + return super(MultiSerializerViewSetMixin, self).get_serializer_class() + + + class NestedViewSetMixin: parent_viewsets = set() From fb2b442b4e088ff6f8800ed7009f30463a7ec65d Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Sun, 24 Apr 2022 23:31:35 +0800 Subject: [PATCH 06/15] fix: check parent permission with model__submodel --- rest_framework_extensions/mixins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 010d6f2..896963c 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -88,11 +88,13 @@ def check_parent_object_permissions(self, request): return current_model = self.get_queryset().model # TODO - # 1. for model__submodel case. + # 1. for model__submodel case(Done). # 2. for generic relations case. for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): - parent_model = current_model._meta.get_field( - parent_model_lookup_name).related_model + parent_model = current_model + for lookup_name in parent_model_lookup_name.split("__"): + parent_model = parent_model._meta.get_field( + lookup_name).related_model for parent_viewset_class in self.parent_viewsets: parent_viewset = parent_viewset_class() parent_viewset_model = getattr( From 3bd757b6225eab2fc9094008fae11ff8c4192d67 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:06:18 +0800 Subject: [PATCH 07/15] fix: permission check error --- rest_framework_extensions/mixins.py | 90 ++++++++++------------------ rest_framework_extensions/routers.py | 9 ++- 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 4610181..64de23a 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin from django.http import Http404 +from django.db import models from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings from rest_framework import status, exceptions @@ -52,40 +53,6 @@ def get_page_size(self, request): # pass -class BulkCreateModelMixin: - """ - Builk create model instance. - Just post data like: - [ - {"name": "xxx"}, - {"name": "xxx2"}, - ] - """ - - def get_serializer(self, *args, **kwargs): - if isinstance(kwargs.get('data', {}), list): - kwargs['many'] = True - s = super().get_serializer(*args, **kwargs) - return s - - -class MultiSerializerViewSetMixin: - """ - serializer_action_classes = { - list: ListSerializer, - : Serializer, - ... - } - """ - serializer_action_classes = {} - def get_serializer_class(self): - try: - return self.serializer_action_classes[self.action] - except (KeyError, AttributeError): - return super(MultiSerializerViewSetMixin, self).get_serializer_class() - - - class NestedViewSetMixin: parent_viewsets = set() @@ -93,14 +60,19 @@ def check_ownership(self, serializer): parent_query_dicts = self.get_parents_query_dict() if parent_query_dicts: parent_name, parent_value = list(parent_query_dicts.items())[-1] - items = serializer.validated_data - if not isinstance(items, list): - items = [items] - for item in items: - if item.get(parent_name, None) is None: + instance_datas = serializer.validated_data + if not isinstance(instance_datas, list): + instance_datas = [instance_datas] + for instance_data in instance_datas: + if instance_data.get(parent_name, None) is None: raise exceptions.PermissionDenied( detail=f"You must specific '{parent_name}'", code=status.HTTP_403_FORBIDDEN) - if item.get(parent_name, None) != parent_value: + received_parent_value = instance_data.get(parent_name, None) + print(received_parent_value) + if not isinstance(received_parent_value, (str, int)): + received_parent_value = getattr( + received_parent_value, self.parent_viewset.lookup_field) + if str(received_parent_value) != str(parent_value): raise exceptions.PermissionDenied( detail=f"You don't have permission to operate item that belone to '{parent_name}:{parent_value}'", code=status.HTTP_403_FORBIDDEN) @@ -112,6 +84,13 @@ def perform_update(self, serializer): self.check_ownership(serializer) super().perform_update(serializer) + def get_parent_model(self, current_model, parent_model_lookup_name): + parent_model = current_model + for lookup_name in parent_model_lookup_name.split("__"): + parent_model = parent_model._meta.get_field( + lookup_name).related_model + return parent_model + def check_parent_object_permissions(self, request): # if parent viewset haven't init yet, then will raise no "kwargs" attribute error, but it doesn't matter, just ignore try: @@ -122,28 +101,25 @@ def check_parent_object_permissions(self, request): return current_model = self.get_queryset().model # TODO - # 1. for model__submodel case(Done). + # 1. for model__submodel case. # 2. for generic relations case. for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): - parent_model = current_model - for lookup_name in parent_model_lookup_name.split("__"): - parent_model = parent_model._meta.get_field( - lookup_name).related_model - for parent_viewset_class in self.parent_viewsets: - parent_viewset = parent_viewset_class() - parent_viewset_model = getattr( - parent_viewset, "model", None) or parent_viewset.queryset.model - if parent_viewset_model == parent_model: - parent_obj = get_object_or_404( - parent_viewset_model.objects.all(), - **{parent_viewset.lookup_field: parent_model_lookup_value} - ) - parent_viewset.check_object_permissions( - request, parent_obj - ) + parent_model = get_parent_model( + current_model, parent_model_lookup_name) + parent_viewset = self.parent_viewset() + parent_viewset_model = getattr( + parent_viewset, "model", None) or parent_viewset.queryset.model + parent_obj = get_object_or_404( + parent_viewset_model.objects.all(), + **{parent_viewset.lookup_field: parent_model_lookup_value} + ) + parent_viewset.check_object_permissions( + request, parent_obj + ) current_model = parent_model def check_permissions(self, request): + print(self.get_permissions()) super().check_permissions(request) if self.parent_viewsets: self.check_parent_object_permissions(request) diff --git a/rest_framework_extensions/routers.py b/rest_framework_extensions/routers.py index df5bd45..68799ca 100644 --- a/rest_framework_extensions/routers.py +++ b/rest_framework_extensions/routers.py @@ -1,3 +1,4 @@ +from copy import deepcopy from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_extensions.utils import compose_parent_pk_kwarg_name @@ -10,19 +11,21 @@ def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None) self.parent_viewset = parent_viewset def register(self, prefix, viewset, basename, parents_query_lookups): + # deepcopy to make sure one viewset class only has one parent viewset + copied_viewset = deepcopy(viewset) self.router._register( prefix=self.get_prefix( current_prefix=prefix, parents_query_lookups=parents_query_lookups), - viewset=viewset, + viewset=copied_viewset, basename=basename, ) - viewset.parent_viewsets.add(self.parent_viewset) + copied_viewset.parent_viewset = self.parent_viewset return NestedRegistryItem( router=self.router, parent_prefix=prefix, parent_item=self, - parent_viewset=viewset + parent_viewset=copied_viewset ) def get_prefix(self, current_prefix, parents_query_lookups): From 5ceb747bd413ea5fbc6d9e09c6d3cd7f9a89da22 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:09:26 +0800 Subject: [PATCH 08/15] fix: delete --- rest_framework_extensions/mixins.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 64de23a..24b4bdd 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -8,6 +8,38 @@ from rest_framework import status, exceptions from rest_framework.generics import get_object_or_404 +class BulkCreateModelMixin: + """ + Builk create model instance. + Just post data like: + [ + {"name": "xxx"}, + {"name": "xxx2"}, + ] + """ + + def get_serializer(self, *args, **kwargs): + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + s = super().get_serializer(*args, **kwargs) + return s + + +class MultiSerializerViewSetMixin: + """ + serializer_action_classes = { + list: ListSerializer, + : Serializer, + ... + } + """ + serializer_classes = {} + def get_serializer_class(self): + try: + return self.serializer_classes[self.action] + except (KeyError, AttributeError): + return super(MultiSerializerViewSetMixin, self).get_serializer_class() + class DetailSerializerMixin: """ From c86a52841460b95ce22d92ff242956a783581366 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:12:19 +0800 Subject: [PATCH 09/15] Update mixins.py --- rest_framework_extensions/mixins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 24b4bdd..b3a3af7 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -151,7 +151,6 @@ def check_parent_object_permissions(self, request): current_model = parent_model def check_permissions(self, request): - print(self.get_permissions()) super().check_permissions(request) if self.parent_viewsets: self.check_parent_object_permissions(request) From 8f763e912d34d33c6f3ab3cbcfa0368a1934e24c Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:18:14 +0800 Subject: [PATCH 10/15] fix: permission check --- rest_framework_extensions/mixins.py | 55 ++++++++++++++++------------ rest_framework_extensions/routers.py | 9 +++-- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 896963c..f2598a1 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin from django.http import Http404 +from django.db import models from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings from rest_framework import status, exceptions @@ -59,14 +60,18 @@ def check_ownership(self, serializer): parent_query_dicts = self.get_parents_query_dict() if parent_query_dicts: parent_name, parent_value = list(parent_query_dicts.items())[-1] - items = serializer.validated_data - if not isinstance(items, list): - items = [items] - for item in items: - if item.get(parent_name, None) is None: + instance_datas = serializer.validated_data + if not isinstance(instance_datas, list): + instance_datas = [instance_datas] + for instance_data in instance_datas: + if instance_data.get(parent_name, None) is None: raise exceptions.PermissionDenied( detail=f"You must specific '{parent_name}'", code=status.HTTP_403_FORBIDDEN) - if item.get(parent_name, None) != parent_value: + received_parent_value = instance_data.get(parent_name, None) + if not isinstance(received_parent_value, (str, int)): + received_parent_value = getattr( + received_parent_value, self.parent_viewset.lookup_field) + if str(received_parent_value) != str(parent_value): raise exceptions.PermissionDenied( detail=f"You don't have permission to operate item that belone to '{parent_name}:{parent_value}'", code=status.HTTP_403_FORBIDDEN) @@ -78,6 +83,13 @@ def perform_update(self, serializer): self.check_ownership(serializer) super().perform_update(serializer) + def get_parent_model(self, current_model, parent_model_lookup_name): + parent_model = current_model + for lookup_name in parent_model_lookup_name.split("__"): + parent_model = parent_model._meta.get_field( + lookup_name).related_model + return parent_model + def check_parent_object_permissions(self, request): # if parent viewset haven't init yet, then will raise no "kwargs" attribute error, but it doesn't matter, just ignore try: @@ -88,28 +100,25 @@ def check_parent_object_permissions(self, request): return current_model = self.get_queryset().model # TODO - # 1. for model__submodel case(Done). + # 1. for model__submodel case. # 2. for generic relations case. for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): - parent_model = current_model - for lookup_name in parent_model_lookup_name.split("__"): - parent_model = parent_model._meta.get_field( - lookup_name).related_model - for parent_viewset_class in self.parent_viewsets: - parent_viewset = parent_viewset_class() - parent_viewset_model = getattr( - parent_viewset, "model", None) or parent_viewset.queryset.model - if parent_viewset_model == parent_model: - parent_obj = get_object_or_404( - parent_viewset_model.objects.all(), - **{parent_viewset.lookup_field: parent_model_lookup_value} - ) - parent_viewset.check_object_permissions( - request, parent_obj - ) + parent_model = get_parent_model( + current_model, parent_model_lookup_name) + parent_viewset = self.parent_viewset() + parent_viewset_model = getattr( + parent_viewset, "model", None) or parent_viewset.queryset.model + parent_obj = get_object_or_404( + parent_viewset_model.objects.all(), + **{parent_viewset.lookup_field: parent_model_lookup_value} + ) + parent_viewset.check_object_permissions( + request, parent_obj + ) current_model = parent_model def check_permissions(self, request): + print(self.get_permissions()) super().check_permissions(request) if self.parent_viewsets: self.check_parent_object_permissions(request) diff --git a/rest_framework_extensions/routers.py b/rest_framework_extensions/routers.py index df5bd45..68799ca 100644 --- a/rest_framework_extensions/routers.py +++ b/rest_framework_extensions/routers.py @@ -1,3 +1,4 @@ +from copy import deepcopy from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_extensions.utils import compose_parent_pk_kwarg_name @@ -10,19 +11,21 @@ def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None) self.parent_viewset = parent_viewset def register(self, prefix, viewset, basename, parents_query_lookups): + # deepcopy to make sure one viewset class only has one parent viewset + copied_viewset = deepcopy(viewset) self.router._register( prefix=self.get_prefix( current_prefix=prefix, parents_query_lookups=parents_query_lookups), - viewset=viewset, + viewset=copied_viewset, basename=basename, ) - viewset.parent_viewsets.add(self.parent_viewset) + copied_viewset.parent_viewset = self.parent_viewset return NestedRegistryItem( router=self.router, parent_prefix=prefix, parent_item=self, - parent_viewset=viewset + parent_viewset=copied_viewset ) def get_prefix(self, current_prefix, parents_query_lookups): From b4aabe44d975e2e554f9722bafde5b92a11afaa3 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:20:41 +0800 Subject: [PATCH 11/15] fix: parent viewset default value --- rest_framework_extensions/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index f2598a1..5f0c439 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -54,7 +54,7 @@ def get_page_size(self, request): class NestedViewSetMixin: - parent_viewsets = set() + parent_viewset = None def check_ownership(self, serializer): parent_query_dicts = self.get_parents_query_dict() From b561c2f21897ce1b3c5cce7fd4a0fc4f67f72437 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:21:27 +0800 Subject: [PATCH 12/15] fix: parent view set default value --- rest_framework_extensions/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index b3a3af7..c9d3ddb 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -86,7 +86,7 @@ def get_page_size(self, request): class NestedViewSetMixin: - parent_viewsets = set() + parent_viewset = None def check_ownership(self, serializer): parent_query_dicts = self.get_parents_query_dict() From cfda43298e38e7099957cc99766e68b6ad9880f3 Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:29:39 +0800 Subject: [PATCH 13/15] fix: typo --- rest_framework_extensions/mixins.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index 5f0c439..f3fe4d5 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -103,7 +103,7 @@ def check_parent_object_permissions(self, request): # 1. for model__submodel case. # 2. for generic relations case. for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): - parent_model = get_parent_model( + parent_model = self.get_parent_model( current_model, parent_model_lookup_name) parent_viewset = self.parent_viewset() parent_viewset_model = getattr( @@ -118,14 +118,13 @@ def check_parent_object_permissions(self, request): current_model = parent_model def check_permissions(self, request): - print(self.get_permissions()) super().check_permissions(request) - if self.parent_viewsets: + if self.parent_viewset: self.check_parent_object_permissions(request) def check_object_permissions(self, request, obj): super().check_object_permissions(request, obj) - if self.parent_viewsets: + if self.parent_viewset: self.check_parent_object_permissions(request) def get_queryset(self): From c17c0ded72bb16efde4d9fd9285b6ed19c478f4a Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Wed, 27 Apr 2022 14:46:50 +0800 Subject: [PATCH 14/15] fix: auto fill parent fields --- rest_framework_extensions/routers.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rest_framework_extensions/routers.py b/rest_framework_extensions/routers.py index 68799ca..3d71104 100644 --- a/rest_framework_extensions/routers.py +++ b/rest_framework_extensions/routers.py @@ -4,19 +4,25 @@ class NestedRegistryItem: - def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None): + def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None, parent_lookups=[]): self.router = router self.parent_prefix = parent_prefix self.parent_item = parent_item self.parent_viewset = parent_viewset + self.parent_lookups = parent_lookups - def register(self, prefix, viewset, basename, parents_query_lookups): + def register(self, prefix, viewset, basename, parents_query_lookups=[], parent_query_lookup=""): # deepcopy to make sure one viewset class only has one parent viewset copied_viewset = deepcopy(viewset) + if not parents_query_lookups: + parents_query_lookups = ["__".join( + [parent_query_lookup, pl]) for pl in self.parent_lookups] + [parent_query_lookup] + self.router._register( prefix=self.get_prefix( current_prefix=prefix, - parents_query_lookups=parents_query_lookups), + parents_query_lookups=parents_query_lookups + ), viewset=copied_viewset, basename=basename, ) @@ -25,7 +31,8 @@ def register(self, prefix, viewset, basename, parents_query_lookups): router=self.router, parent_prefix=prefix, parent_item=self, - parent_viewset=copied_viewset + parent_viewset=copied_viewset, + parent_lookups=parents_query_lookups ) def get_prefix(self, current_prefix, parents_query_lookups): From 917071b6353bbcadff67bc8133e3608fa5c92e1b Mon Sep 17 00:00:00 2001 From: "zhibing.chen" <87839912+sobadgirl@users.noreply.github.com> Date: Fri, 29 Apr 2022 23:01:38 +0800 Subject: [PATCH 15/15] fix: permisson chain check error --- rest_framework_extensions/mixins.py | 92 ++++++++++++++++-------- rest_framework_extensions/routers.py | 22 ++++-- rest_framework_extensions/serializers.py | 33 +++++++++ 3 files changed, 111 insertions(+), 36 deletions(-) diff --git a/rest_framework_extensions/mixins.py b/rest_framework_extensions/mixins.py index df62ab2..3f80e4d 100644 --- a/rest_framework_extensions/mixins.py +++ b/rest_framework_extensions/mixins.py @@ -1,9 +1,4 @@ -from rest_framework_extensions.cache.mixins import CacheResponseMixin -from django.core.exceptions import ValidationError -# from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin from django.http import Http404 -from django.db import models -from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin from rest_framework_extensions.settings import extensions_api_settings from rest_framework import status, exceptions from rest_framework.generics import get_object_or_404 @@ -90,22 +85,57 @@ class NestedViewSetMixin: def check_ownership(self, serializer): parent_query_dicts = self.get_parents_query_dict() - if parent_query_dicts: - parent_name, parent_value = list(parent_query_dicts.items())[-1] - instance_datas = serializer.validated_data - if not isinstance(instance_datas, list): - instance_datas = [instance_datas] - for instance_data in instance_datas: - if instance_data.get(parent_name, None) is None: - raise exceptions.PermissionDenied( - detail=f"You must specific '{parent_name}'", code=status.HTTP_403_FORBIDDEN) - received_parent_value = instance_data.get(parent_name, None) - if not isinstance(received_parent_value, (str, int)): - received_parent_value = getattr( - received_parent_value, self.parent_viewset.lookup_field) - if str(received_parent_value) != str(parent_value): + if not parent_query_dicts: + return + + parent_lookup, parent_value = list(parent_query_dicts.items())[-1] + if "__" in parent_lookup: + receive_key, _ = parent_lookup.split("__") + else: + receive_key = parent_lookup + + instance_datas = serializer.validated_data + if not isinstance(instance_datas, list): + instance_datas = [instance_datas] + received_parent_values = [ + i.get(receive_key) for i in instance_datas if i.get(receive_key)] + + # 1. check filled parent field + if len(received_parent_values) != len(instance_datas): + raise exceptions.PermissionDenied( + detail=f"You must specific '{parent_lookup}'", code=status.HTTP_403_FORBIDDEN) + + received_parent_values = [str(v) if isinstance(v, (str, int)) else + str(getattr(v, self.parent_viewset.lookup_field)) + for v in received_parent_values + ] + # 2. check direct FK parent + if not "__" in parent_lookup: + not_blong = [ + v for v in received_parent_values if v != str(parent_value) + ] + if not_blong: + raise exceptions.PermissionDenied( + detail=f"You don't have permission to operate item that belong to '{parent_lookup}:{not_blong}'", code=status.HTTP_403_FORBIDDEN) + else: + # 3. for multiple layer parent + direct_parent, direct_parent_look_field = parent_lookup.split( + '__', 1) + current_model = self.get_queryset().model + direct_parent_model = current_model._meta.get_field( + direct_parent + ).related_model + direct_parent_instances = direct_parent_model.objects.filter( + **{f"pk__in": received_parent_values} + ) + fields = direct_parent_look_field.split("__") + for instance in direct_parent_instances: + final_parent_obj = instance + for f in fields: + final_parent_obj = getattr(instance, f) + if (received_value := str(getattr(final_parent_obj, self.parent_viewset.lookup_field))) != str(parent_value): raise exceptions.PermissionDenied( - detail=f"You don't have permission to operate item that belone to '{parent_name}:{parent_value}'", code=status.HTTP_403_FORBIDDEN) + detail=f"You don't have permission to operate item that belong to '{parent_lookup}:{received_value}'", code=status.HTTP_403_FORBIDDEN) def perform_create(self, serializer): self.check_ownership(serializer) @@ -125,28 +155,28 @@ def get_parent_model(self, current_model, parent_model_lookup_name): def check_parent_object_permissions(self, request): # if parent viewset haven't init yet, then will raise no "kwargs" attribute error, but it doesn't matter, just ignore try: - parents_query_dict = self.get_parents_query_dict() + if not (parents_query_dict := self.get_parents_query_dict()): + return except: return - if not parents_query_dict: - return - current_model = self.get_queryset().model - # TODO # 2. for generic relations case. - for parent_model_lookup_name, parent_model_lookup_value in reversed(parents_query_dict.items()): + current_model = self.get_queryset().model + current_viewset = self + + for parent_model_lookup_name, parent_model_lookup_value in sorted(parents_query_dict.items(), key=lambda item: len(item[0])): parent_model = self.get_parent_model( current_model, parent_model_lookup_name) - parent_viewset = self.parent_viewset() - parent_viewset_model = getattr( - parent_viewset, "model", None) or parent_viewset.queryset.model + parent_viewset = current_viewset.parent_viewset() + parent_obj = get_object_or_404( - parent_viewset_model.objects.all(), + parent_model.objects.all(), **{parent_viewset.lookup_field: parent_model_lookup_value} ) parent_viewset.check_object_permissions( request, parent_obj ) - current_model = parent_model + + current_viewset = parent_viewset def check_permissions(self, request): super().check_permissions(request) diff --git a/rest_framework_extensions/routers.py b/rest_framework_extensions/routers.py index 68799ca..121e6ab 100644 --- a/rest_framework_extensions/routers.py +++ b/rest_framework_extensions/routers.py @@ -4,28 +4,40 @@ class NestedRegistryItem: - def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None): + def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None, parent_lookups=[]): self.router = router self.parent_prefix = parent_prefix self.parent_item = parent_item self.parent_viewset = parent_viewset + self.parent_lookups = parent_lookups - def register(self, prefix, viewset, basename, parents_query_lookups): + def register(self, prefix, viewset, basename, parents_query_lookups=[], parent_query_lookup=""): # deepcopy to make sure one viewset class only has one parent viewset - copied_viewset = deepcopy(viewset) + copied_viewset = type(viewset.__name__, (viewset,), { + k: v for k, v in viewset.__dict__.items()}) + if not parents_query_lookups: + parents_query_lookups = ["__".join( + [parent_query_lookup, pl]) for pl in self.parent_lookups] + [parent_query_lookup] + self.router._register( prefix=self.get_prefix( current_prefix=prefix, - parents_query_lookups=parents_query_lookups), + parents_query_lookups=parents_query_lookups + ), viewset=copied_viewset, basename=basename, ) copied_viewset.parent_viewset = self.parent_viewset + v = copied_viewset + while v.parent_viewset: + v = v.parent_viewset + return NestedRegistryItem( router=self.router, parent_prefix=prefix, parent_item=self, - parent_viewset=copied_viewset + parent_viewset=copied_viewset, + parent_lookups=parents_query_lookups ) def get_prefix(self, current_prefix, parents_query_lookups): diff --git a/rest_framework_extensions/serializers.py b/rest_framework_extensions/serializers.py index 09ecf0d..4dc4336 100644 --- a/rest_framework_extensions/serializers.py +++ b/rest_framework_extensions/serializers.py @@ -29,6 +29,39 @@ def get_fields_for_partial_update(opts, init_data, fields, init_files=None): return sorted(set(update_fields)) +class BulkCreateModelMixin: + """ + Builk create model instance. + Just post data like: + [ + {"name": "xxx"}, + {"name": "xxx2"}, + ] + """ + + def get_serializer(self, *args, **kwargs): + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + s = super().get_serializer(*args, **kwargs) + return s + + +class MultiSerializerViewSetMixin: + """ + serializer_action_classes = { + list: ListSerializer, + : Serializer, + ... + } + """ + serializer_classes = {} + def get_serializer_class(self): + try: + return self.serializer_classes[self.action] + except (KeyError, AttributeError): + return super(MultiSerializerViewSetMixin, self).get_serializer_class() + + class PartialUpdateSerializerMixin: def save(self, **kwargs): self._update_fields = kwargs.get('update_fields', None)