Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: make self.request available within javascript_exclude class properties #650

Open
jacksund opened this issue Feb 5, 2024 · 10 comments
Assignees

Comments

@jacksund
Copy link
Contributor

jacksund commented Feb 5, 2024

When I set an attribute dynamically via a @property attribute and also add it to javascript_exclude, the request is no longer accessible from self.

Here's the minimal amount of code to reproduce:

class Example(UnicornView):

    class Meta:
        javascript_exclude = (
            "user_id",
        )

    @property
    def user_id(self):
        return self.request.user.id

This is a silly example (since request.user.id is already accessible in the template). In a real-world example, I would use the request.user within a property to grab things like user settings and/or database objects related to user + add extra python logic.

Is there a way to implement this?

@adamghill
Copy link
Owner

Hey, that's pretty weird and unexpected. I'll try to take a look and see if it's a straight-forward fix over the next couple of days.

@adamghill adamghill self-assigned this Feb 5, 2024
@jacksund
Copy link
Contributor Author

jacksund commented Feb 5, 2024

awesome, thank you for taking a look 😄

@adamghill
Copy link
Owner

Sorry, this took longer than I was expecting because I'm having trouble replicating this. #653 has an example that is basically what you were trying. It looks to me like self.request is available. Maybe I'm missing something, though?

Screenshot 2024-02-11 at 3 17 31 PM

@jacksund
Copy link
Contributor Author

jacksund commented Feb 12, 2024

This is odd because that exact same view doesn't work in my setup. The main difference with my server is that I use django-allauth on top of django's default auth backend, so maybe the allauth middleware is doing something unexpected...? I will start from scratch (a new django project with only unicorn added) -- hopefully that tells us if the issue is just with my other django apps (like allauth).

extra notes

To help debug, I tried adding the following print statements to get_frontend_context_variables:

def get_frontend_context_variables(self) -> str:

# method is identical besides print statements
if isinstance(self.Meta.javascript_exclude, Sequence):
    print(f"frontend_context_variables: {frontend_context_variables}")
    print(f"Attempt property access: {self.user}")
    # ...

And the output was...

frontend_context_variables: {}
Attempt property access: AnonymousUser

So for some reason the property works as expect (it prints AnonymousUser or the actual user normally), but the attribute user is not showing up in the frontend_context_variables.

This leads to the following error + traceback:

Internal Server Error: /apps/spotfire/debugging/
Traceback (most recent call last):
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django\core\handlers\base.py", line 220, in _get_response
    response = response.render()
               ^^^^^^^^^^^^^^^^^
  File "<decorator-gen-20>", line 2, in render
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django_unicorn\decorators.py", line 20, in timed
    result = func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django_unicorn\components\unicorn_template_response.py", line 138, in render
    frontend_context_variables = self.component.get_frontend_context_variables()
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-26>", line 2, in get_frontend_context_variables
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django_unicorn\decorators.py", line 20, in timed
    result = func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\nxj625\AppData\Local\anaconda3\envs\simmate_dev\Lib\site-packages\django_unicorn\components\unicorn_view.py", line 452, in get_frontend_context_variables
    raise serializer.InvalidFieldNameError(
django_unicorn.serializer.InvalidFieldNameError: Cannot resolve 'user'.

@jacksund
Copy link
Contributor Author

I'm still getting the error in a clean setup. So I was wrong in my previous comment about allauth or another app causing the problem. Something else is off 😢

I used our basic view inside an otherwise empty django setup:
mysite.zip

Django: v4.2.7
Django-unicorn: v0.58.0

When you read through those files, you'll see it's everything from the default "start project" command in django, plus the view in question. And once you start the server, you can see the error at http://127.0.0.1:8000/polls/

@adamghill
Copy link
Owner

Thanks for the sample code and all of the details. So, I think there are a few things going on here.

I assume you saw this warning and were trying to put a property into javascript_excludes and that was causing the InvalidFieldNameError.
image
I think this warning is incorrect and I should remove it from the documentation. I have a feeling it was true in a previous version of django-unicorn, but not anymore.

https://www.django-unicorn.com/docs/views/?highlight=property#javascript-exclude is only applicable for attributes, not methods or properties. As far as I can tell, properties are not callable from the frontend at all.

I tested with the following changes to views.py:
image
index.html:
image

End result:
image

You can see that user_property doesn't result in anything. However, you can also see that user_attribute value is included in the unicorn:data which may or may not be acceptable, depending on the data. To prevent it from being in the source, javascript_exclude = ("user_attribute",) can be used.

Hopefully, that all makes sense. Let me know if there are documentation changes I can make (other than the blatant one I mentioned above) that could make this more clear.

@jacksund
Copy link
Contributor Author

As far as I can tell, properties are not callable from the frontend at all. ... You can see that user_property doesn't result in anything.

I've been using properties a ton in my views, and they seem to work fine as long as I don't try to use self.request within the property definition. For example:

from django_unicorn.components import UnicornView

class Debug(UnicornView):
    template_name = "polls/index.html"

    class Meta:
        javascript_exclude = ()
  
    @property
    def basic_property(self):
        return 12345
    
    @property
    def user_property(self):
        return str(self.request.user)
{% load unicorn %}
<html>
  <head>
    {% unicorn_scripts %}
  </head>
  <body>
    {% csrf_token %}
    <div unicorn:view>
        Basic Property: {{ basic_property }}
        <br>
        User Property: {{ user_property }}
    </div>
  </body>
</html>

image

You can see basic_property worked as if it was an attribute. Then user_property "failed" but did so silently. When I add basic_property to javascript_exclude, it works as expected too. But when I user_property to javascript_exclude, it raises the error.

So I assumed attributes and properties behaved the same across all django-unicorn (unless self.request gets involved). Is this not normal?

I assume you saw this warning and were trying to put a property into javascript_excludes and that was causing the InvalidFieldNameError

Not exactly 😅. I'm putting properties in javascript_excludes because they are

  • only needed in the template rendering
  • constants (once dynamically determined)
  • very large values that slow down the ajax calls if included

I used self.request.user as an example to keep the code simple, but in my application, a more realistic view look is similar to your search example except where the searched list is dynamically determined based on the user:

from functools import cached_property

class Example(UnicornView):

    class Meta:
        javascript_exclude = ("user_chemicals",)
    
    @cached_property
    def user_chemicals(self) -> list:
        """
        a list of ~100-300 chemical names that are assigned
        to this user and/or the user's team
        """
        user = self.request.user
        chemicals = user.assigned_chemicals.filter(....).all()
        return chemicals
   
    # then there are a series of normal methods/attributes that
    # handle how `user_chemicals` are used within an
    # interactive form. For example, one of these chemicals
    # can be selected from a dropdown menu, and set to
    # an attribute:
    selected_chemical_id = None

So I'm using cached_property and only using this long list of user_chemicals objects in the template.

@jacksund
Copy link
Contributor Author

also I'm sorry for my long comments! Hopefully they aren't too much... I really appreciate you taking the time to read through and help me out 😊

@adamghill
Copy link
Owner

I've been using properties a ton in my views, and they seem to work fine

Oh my gosh, ok. 🤦 Well, at least that fits into what I would have expected! Sorry for that red herring around properties -- it's been a while since I've used a property in a component.

Thanks for being patient and explaining the situation. I have a few potential leads now and I'm going to see if I can figure out what is going on.

@jacksund
Copy link
Contributor Author

fyi - I have a workaround for this issue. It requires a couple lines of extra boilerplate, but it gets the job done:

from functools import cached_property

class Example(UnicornView):

    class Meta:
        javascript_exclude = ("user_chemicals",)
    
    user_chemicals: list = None
    
    def mount(self):
        self.user_chemicals = self.get_user_chemicals()

    def get_user_chemicals(self) -> list:
        user = self.request.user
        chemicals = user.assigned_chemicals.filter(....).all()
        return chemicals

So instead of using @cached_property, you need to (1) define a method that gets your property and (2) call that method & assign it in mount. Why this works while a @cached_property doesn't is lost on me though 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants