9

What I am trying to do:

I am trying to access request object in my django models so that I can get the currently logged in user with request.user.

What I have tried:

I found a hack on this site. But someone in the comments pointed out not to do it when in production.

I also tried to override model's __init__ method just like mentioned in this post. But I got an AttributeError: 'RelatedManager' object has no attribute 'request'

Models.py:

class TestManager(models.Manager):
    def user_test(self):
        return self.filter(user=self.request.user, viewed=False)

class Test(models.Model):

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(Test, self).__init__(*args, **kwargs)

    user = models.ForeignKey(User, related_name='test') 
    viewed = models.BooleanField(default=False)
    objects = TestManager()
Ahtisham
  • 9,170
  • 4
  • 43
  • 57
  • 1
    Usually the model layer should *never* have access to the `request` object. The idea is that a model layer only models business logic, the views are used to implement "use cases". So doing this would actually break the "layers". `Model`s and their relations are *not* request aware, since the models are not per se handled with a request (for example through a command). – Willem Van Onsem Sep 07 '18 at 09:50
  • Furthermore the quoated article is in fact very problematic: in case the app handles concurrent requests, this can go really bad, as in that the request is the latest request that passes through the middleware, not the one that is handled by the control flow. – Willem Van Onsem Sep 07 '18 at 09:53

1 Answers1

13

I trying to access request object in my Django models so that I can get the currently logged in user with request.user.

Well a problem is that models are not per se used in the context of a request. One for example frequently defines custom commands to do bookkeeping, or one can define an API where for example the user is not present. The idea of the Django approach is that models should not be request-aware. Models define the "business logic" layer: the models define entities and how they interact. By not respecting these layers, one makes the application vulnerable for a lot of problems.

The blog you refer to aims to create what they call a global state (which is a severe anti-patten): you save the request in the middleware when the view makes a call, such that you can then fetch that object in the model layer. There are some problems with this approach: first of all, like already said, not all use cases are views, and thus not all use cases pass through the middleware. It is thus possible that the attribute does not exist when fetching it.

Furthermore it is not guaranteed that the request object is indeed the request object of the view. It is for example possible that we use the model layer with a command that thus does not pass through the middleware, in which case we should use the previous view request (so potentially with a different user). If the server processes multiple requests concurrently, it is also possible that a view will see a request that arrived a few nanoseconds later, and thus again take the wrong user. It is also possible that the authentication middleware is conditional, and thus that not all requests have a user attribute. In short there are more than enough scenario's where this can fail, and the results can be severe: people seeing, editing, or deleting data that they do not "own" (have no permission to view, edit, or delete).

You thus will need to pass the request, or user object to the user_test method. For example with:

from django.http import HttpRequest

class TestManager(models.Manager):
    def user_test(self, request_or_user):
        if isinstance(request_or_user, HttpRequest):
            return self.filter(user=request_or_user.user, viewed=False)
        else:
            return self.filter(user=request_or_user, viewed=False)

one thus has to pass the request object from the view to the function. Even this is not really pure. A real pure approach would only accept a user object:

class TestManager(models.Manager):
    def user_test(self, user):
            return self.filter(user=user, viewed=False)

So in a view one can use this as:

def some_view(request):
    some_tests = Test.objects.user_test(request.user)
    # ...
    # return Http response

For example if we want to render a template with this queryset, we can pass it like:

def some_view(request):
    some_tests = Test.objects.user_test(request.user)
    # ...
    return render(request, 'my_template.html', {'some_tests': some_tests})
djvg
  • 11,722
  • 5
  • 72
  • 103
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • Before you posted the answer I was trying to create a function in views which would return user object and then importing that function inside my model. But I am not able to import the function in my model: `from .views import get_user` failing with `ImportError`. – Ahtisham Sep 07 '18 at 11:00
  • Is is logical that you can not import a function from the view in models. Usually views should depend on models, not vice versa. You probably constructed a cyclic import. – Willem Van Onsem Sep 07 '18 at 11:02
  • 1
    Okay Sheldon ;) I tried your solution too but in the templates while accessing `user_test` returns nothing. Usage in my template: `{% if request.user.test.user_test %} Yes {% else %} No {% endif %}` always gives `No`. – Ahtisham Sep 07 '18 at 11:08
  • Well unfortunately that is another anti-pattern :( https://softwareengineering.stackexchange.com/a/250148/166310. You should calculate this in the view, so something like `mytest = Test.objects.user_test(request.user)` and then pass this variable. – Willem Van Onsem Sep 07 '18 at 11:10
  • No, here `mytest` is the *result* of a call. As you can see, where here call `objects.user_test(..)`. – Willem Van Onsem Sep 07 '18 at 11:20
  • Dr. Cooper :D you forgot to format the code in your answer. – Ahtisham Sep 07 '18 at 11:24