Django #property calculating a model field: FieldError: Cannot resolve keyword - python

I'm following the method used by #Yauhen Yakimovich in this question:
do properties work on django model fields?
To have a model field that is a calculation of a different model.
The Problem:
FieldError: Cannot resolve keyword 'rating' into field. Choices are: _rating
The rating model field inst correctly hidden and overridden by my rating property causing an error when I try to access it.
My model:
class Restaurant(models.Model):
...
...
#property
def rating(self):
from django.db.models import Avg
return Review.objects.filter(restaurant=self.id).aggregate(Avg('rating'))['rating__avg']
Model in Yauhen's answer:
class MyModel(models.Model):
__foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
#property
def foo(self):
if self.bar:
return self.bar
else:
return self.__foo
#foo.setter
def foo(self, value):
self.__foo = value
Any ideas on how to correctly hid the rating field and define the #property technique?

Solved by using sorted()
I was using a query with order_by() to call rating. order_by() is at the database level and doesnt know about my property. Soultion, use Python to sort instead:
sorted(Restaurant.objects.filter(category=category[0]), key=lambda x: x.rating, reverse=True)[:5]
If you encounter a similar error check through your views for anything that might be calling the property. Properties will no longer work at the datatbase level.

Change this line:
self._rating = Review.objects.filter(restaurant=self.id).aggregate(Avg('rating'))['rating__avg']
into this one:
self._rating = Review.objects.filter(restaurant=self.id).aggregate(Avg('_rating'))['_rating__avg']
(notice change of reference in query from rating and rating__avg to _rating and _rating__avg)

Related

Can you use a class object as property in Django?

I have three models in my Django application - simplified for demonstration. The first one is Base. The second one is Bill and has foreign key "base" to Base model. The third one is Point, and has a foreign key to Bill.
class Base(models.Model):
type = models.CharField()
desc = models.IntegerField()
class Bill(models.Model):
base = models.ForeignKey("Base", related_name="bills")
total_value = models.DecimalField()
class Point(models.Model):
bill = models.ForeignKey("Bill", related_name="points")
value = models.DecimalField()
My goal is to have a property for Base called def bill(self), that would sum up all "bill" objects fields and returned a bill-like instance. I would like to get a base object:
my_base = Base.objects.get()
bill = Base.bill <-- I can figure this out with #property
points = bill.points <-- this does not work through #property function.
My current solution is like this, to get the first part working:
class Base():
...
#property
def bill(self):
sums = self.bills.aggregate(total_value=Sum('total_value'))
return Bill(**sums)
The second part, that would sum up bill-related-points and return them in a my_base.bill.points, does not work. If I filter for points and try to assign them to Bill(**sums).points = filtered_points, I get an error: Direct assignment to the reverse side is prohibited, or Unsaved model instance cannot be used in an ORM query
Is there a more elegant solution to this? A good option would be to initate class as a #property like so:
class Base(model.Model):
...
class bill(...):
self.total_value = .
self.points = .
but I don't believe that is achievable.
Thank you

python getting many to many from model mixin returning None

I have a model:
class Employee(models.Model, MyMixin):
full_name = models.CharField(max_length=255)
items = models.ManyToManyField(Item, blank=True)
and a mixin class:
class MyMixin(object):
def my_m2m(self, field):
field_value = getattr(self, field)
print(field_value)
// do something with many to many field
emp = Employee()
emp.my_m2m("items")
It gives me the result like employee.Item.None while printing emp.my_m2m("items")
on console.
If I do emp.items.all() it gives me the result but I cant get it by name.
Why is it not giving the list of item associated with it ?
Am I missing anything ?
As you say, adding .all() gives the result, so you need to add that to your dynamic lookup:
field_value = getattr(self, field).all()

django-filter and DRF: AND clause in a lookupfield

This question expands on this one.
Suppose my models are
class Car(models.Model):
images = models.ManyToManyField(Image)
class Image(models.Model):
path = models.CharField()
type = models.CharField()
and my filter class is
class CarFilter(django_filters.FilterSet):
having_image = django_filters.Filter(name="images", lookup_type='in')
class Meta:
model = Car
then I can make GET queries like ?having_image=5 and get all cars that have an image with pk=5. That's fine. But what if I need to return both cars with this image AND also cars with no images at all, in one list? How do I unite two conditions in one django_filters.Filter?
I don't think that's supported out of the box with django-filter. You could try to create your own Filter class and override the filter() method.
Querysets do support the | (union) operator, so you can combine the default result with the results where the field result is None.
class AndNoneFilter(django_filters.Filter):
def filter(self, qs, value):
return qs(**{self.field_name: None}) | super().filter(qs, value)
class CarFilter(django_filters.FilterSet):
having_image = AndNoneFilter(field_name='images', lookup_type='in')
I've not tested this code, so it might not work exactly as written, but you get the idea.

django - a model field which gets the result of model method

I have this model
class Home(models.Model):
...
number_female = models.IntegerField()
number_male = models.IntegerField()
def all_people(self):
return self.number_female + self.number_male
all_members = all_people(self)
i am getting: name 'self' is not defined.
how can I define a field which gets the result of models method? this Home scenario is just an example, i have more complex models, i just wanted to make question clearer.
If you would like to add a calculated field like all_members as a part of your model, then you will have to override the save function:
class Home(models.Model):
...
all_members = models.IntegerField()
def save(self):
all_members = self.all_people()
super(Home, self).save()
Now you can filter by all_members. It would be better to use the #property decorator for all_members, in this case.
Another approach would be to use Django's extra method as mentioned in a different stackoverflow answer
You still need to define all_members as a model field (not as an integer), and then populate it with the desired value when you save() the instance.
class Home(models.Model):
...
number_female = models.IntegerField()
number_male = models.IntegerField()
all_members = models.IntegerField()
def save(self):
self.all_members = self.number_female + self.number_male
super(Home, self).save()
I think Django Managers can be a solution here. Example:
Custom Manager:
class CustomFilter(models.Manager):
def all_people(self):
return self.number_female + self.number_male
Model:
class Home(models.Model):
....
objects= CustomFilter()
Views:
allpeople= Home.objects.all_people(Home.objects.all())

Do properties work on Django model fields?

I think the best way to ask this question is with some code... can I do this:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self.foo
def set_foo(self, input):
self.foo = input
foo = property(get_foo, set_foo)
or do I have to do it like this:
class MyModel(models.Model):
_foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self._foo
def set_foo(self, input):
self._foo = input
foo = property(get_foo, set_foo)
note: you can keep the column name as 'foo' in the database by passing a db_column to the model field. This is very helpful when you are working on an existing system and you don't want to have to do db migrations for no reason
A model field is already property, so I would say you have to do it the second way to avoid a name clash.
When you define foo = property(..) it actually overrides the foo = models.. line, so that field will no longer be accessible.
You will need to use a different name for the property and the field. In fact, if you do it the way you have it in example #1 you will get an infinite loop when you try and access the property as it now tries to return itself.
EDIT: Perhaps you should also consider not using _foo as a field name, but rather foo, and then define another name for your property because properties cannot be used in QuerySet, so you'll need to use the actual field names when you do a filter for example.
As mentioned, a correct alternative to implementing your own django.db.models.Field class, one should use the db_column argument and a custom (or hidden) class attribute. I am just rewriting the code in the edit by #Jiaaro following more strict conventions for OOP in python (e.g. if _foo should be actually hidden):
class MyModel(models.Model):
__foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
#property
def foo(self):
if self.bar:
return self.bar
else:
return self.__foo
#foo.setter
def foo(self, value):
self.__foo = value
__foo will be resolved into _MyModel__foo (as seen by dir(..)) thus hidden (private). Note that this form also permits using of #property decorator which would be ultimately a nicer way to write readable code.
Again, django will create _MyModel table with two fields foo and bar.
The previous solutions suffer because #property causes problems in admin, and .filter(_foo).
A better solution would be to override setattr except that this can cause problems initializing the ORM object from the DB. However, there is a trick to get around this, and it's universal.
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def __setattr__(self, attrname, val):
setter_func = 'setter_' + attrname
if attrname in self.__dict__ and callable(getattr(self, setter_func, None)):
super(MyModel, self).__setattr__(attrname, getattr(self, setter_func)(val))
else:
super(MyModel, self).__setattr__(attrname, val)
def setter_foo(self, val):
return val.upper()
The secret is 'attrname in self.__dict__'. When the model initializes either from new or hydrated from the __dict__!
It depends whether your property is a means-to-an-end or an end in itself.
If you want this kind of "override" (or "fallback") behavior when filtering querysets (without first having to evaluate them), I don't think properties can do the trick. As far as I know, Python properties do not work at the database level, so they cannot be used in queryset filters. Note that you can use _foo in the filter (instead of foo), as it represents an actual table column, but then the override logic from your get_foo() won't apply.
However, if your use-case allows it, the Coalesce() class from django.db.models.functions (docs) might help.
Coalesce() ... Accepts a list of at least two field names or
expressions and returns the first non-null value (note that an empty
string is not considered a null value). ...
This implies that you can specify bar as an override for foo using Coalesce('bar','foo'). This returns bar, unless bar is null, in which case it returns foo. Same as your get_foo() (except it doesn't work for empty strings), but on the database level.
The question that remains is how to implement this.
If you don't use it in a lot of places, simply annotating the queryset may be easiest. Using your example, without the property stuff:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Then make your query like this:
from django.db.models.functions import Coalesce
queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo'))
Now the items in your queryset have the magic attribute bar_otherwise_foo, which can be filtered on, e.g. queryset.filter(bar_otherwise_foo='what I want'), or it can be used directly on an instance, e.g. print(queryset.all()[0].bar_otherwise_foo)
The resulting SQL query from queryset.query shows that Coalesce() indeed works at the database level:
SELECT "myapp_mymodel"."id", "myapp_mymodel"."foo", "myapp_mymodel"."bar",
COALESCE("myapp_mymodel"."bar", "myapp_mymodel"."foo") AS "bar_otherwise_foo"
FROM "myapp_mymodel"
Note: you could also call your model field _foo then foo=Coalesce('bar', '_foo'), etc. It would be tempting to use foo=Coalesce('bar', 'foo'), but that raises a ValueError: The annotation 'foo' conflicts with a field on the model.
There must be several ways to create a DRY implementation, for example writing a custom lookup, or a custom(ized) Manager.
A custom manager is easily implemented as follows (see example in docs):
class MyModelManager(models.Manager):
""" standard manager with customized initial queryset """
def get_queryset(self):
return super(MyModelManager, self).get_queryset().annotate(
bar_otherwise_foo=Coalesce('bar', 'foo'))
class MyModel(models.Model):
objects = MyModelManager()
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Now every queryset for MyModel will automatically have the bar_otherwise_foo annotation, which can be used as described above.
Note, however, that e.g. updating bar on an instance will not update the annotation, because that was made on the queryset. The queryset will need to be re-evaluated first, e.g. by getting the updated instance from the queryset.
Perhaps a combination of a custom manager with annotation and a Python property could be used to get the best of both worlds (example at CodeReview).

Categories