Open jacksund opened 8 months ago
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.
awesome, thank you for taking a look 😄
Sorry, this took longer than I was expecting because I'm having trouble replicating this. https://github.com/adamghill/django-unicorn/pull/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?
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).
To help debug, I tried adding the following print statements to get_frontend_context_variables
:
# 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'.
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/
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
.
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
:
index.html
:
End result:
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.
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>
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
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.
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 😊
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.
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 🤷
When I set an attribute dynamically via a
@property
attribute and also add it tojavascript_exclude
, therequest
is no longer accessible fromself
.Here's the minimal amount of code to reproduce:
This is a silly example (since
request.user.id
is already accessible in the template). In a real-world example, I would use therequest.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?