Django & rest_framework - method get_queryset being called twice, is there any other better solution?
Asked Answered
S

1

0

Currently I have a project using django and rest_framework to get some basic APIs on the run.

The problem is, when i make a view using the generic lib on rest_framework and DjangoModelPermissions, my method get_queryset is being called twice

My Permission Class

class DefaultModelPermissions(DjangoModelPermissions):
    """
    The request is authenticated using `django.contrib.auth` permissions.
    See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions

    It ensures that the user is authenticated, and has the appropriate
    `view`/`add`/`change`/`delete` permissions on the model.

    This permission can only be applied against view classes that
    provide a `.queryset` attribute.
    """
    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
        'HEAD': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

class DefaultModelViewPermissions:
    permission_classes = (DefaultModelPermissions,)

My View

class AgentListCreateView(DefaultModelViewPermissions, generics.ListCreateAPIView):
    serializer_class = serializers.AgentSerializer
    
    def get_queryset(self):
         print('get_queryset')
         return models.Agent.objects.all()

Debbuging the app, I found out that the permission also call the get_queryset to retrieve the model to validate the django's user's permissions.

permission_calling_get_queryset

From my point of view, this is an unecessary operation. Deppending on the size of the DB table, this could be a major performance problem, and leed many other app issues.

My solution was to override this "has_permission" method. So my class got like

class DefaultModelPermissions(DjangoModelPermissions):
    """
    The request is authenticated using `django.contrib.auth` permissions.
    See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions

    It ensures that the user is authenticated, and has the appropriate
    `view`/`add`/`change`/`delete` permissions on the model.

    This permission can only be applied against view classes that
    provide a `.queryset` attribute.
    """
    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
        'HEAD': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }
    
    def has_permission(self, request, view):
        if not request.user or (
           not request.user.is_authenticated and self.authenticated_users_only):
            return False

        # Workaround to ensure DjangoModelPermissions are not applied
        # to the root view when using DefaultRouter.
        if getattr(view, '_ignore_model_permissions', False):
            return True

        # Workaround cause base class utilized get_queryset to retrieve the model
        # making unecessary requisitions to the database
        if hasattr(view, 'get_serializer_class'):
            model = view.get_serializer_class().Meta.model
        else:
            model = view.serializer_class.Meta.model
        
        perms = self.get_required_permissions(request.method, model)

        return request.user.has_perms(perms)

My question is: Is there a better way to get this around? Does anybody else got the same problem?

Sika answered 30/7 at 2:7 Comment(0)
A
0

Answer

  1. There is no need to create DefaultModelViewPermissions class (If you have no non-model views). Just use built in DRF settings:
# settings.py

'DEFAULT_PERMISSION_CLASSES': [
    ...
   'path.to.DefaultModelPermissions',
   ...
]
  1. DjangoModelPermissions.has_permission method not executing the DB query. It's getting queryset to use the model_class from it without evaluating.

*Django DOCS*

Internally, a QuerySet can be constructed, filtered, sliced, and generally passed around without actually hitting the database. No database activity actually occurs until you do something to evaluate the queryset.

You can evaluate a QuerySet in the following ways:

  • Iteration
  • Asynchronous iteration
  • Slicing
  • Pickling/Caching
  • repr()
  • len()
  • list()
  • bool()

Long story short: There is no need to customize has_permission method.

Alkali answered 30/7 at 2:59 Comment(2)
Thanks! I didn't want to add it to 'DEFAULT_PERMISSION_CLASSES' cause it's a model based permission and we are going to have some views not based on models. The qs evaluationg doc is going to be really helpfull!Sika
@Sika glad that my answer helped you. I also updated it with remark about views not based on model.Alkali

© 2022 - 2024 — McMap. All rights reserved.