Calling a model's method from another model - django

I am quite new to django so my question might seem very trivial..
I have 2 models (simplified for demonstration purposes):
class Subarticle(models.Model):
parent_article = models.ForeignKey(Article, related_name='subarticles')
priority = IntegerField()
....
def getCheapest(self, quantity): //find cheapest subarticle based on qty
//code
and
class Article(models.Model):
sub_article_qty = models.IntegerField()
def production_cost(self):
sub_article = Subarticle.objects.filter(parent_article=self).order_by('priority').first
sub_article_price = sub_article.getCheapest(self.sub_article_qty)
return sub_article_price*self.sub_article_qty
So basically every article has one or more sub-articles and I want to be able to find the cost for the article based on the cheapest priced sub-article with the lowest priority number.
I am using "rest_framwork" to send the model data with approximately following serializer
from .models import Subarticles, Articles
from rest_framwork import serializers
class SubarticleSerializer(serializer.HyperlinkedModelSerializer):
class Meta:
model=Subarticle
fields=('parent_article','priority')
class ArticleSerializer(serializer.HyperlinkedModelSerializer):
class Meta:
model=Article
fields=('sub_article_qty','subarticles','production_cost')
But trying to do this like this gives me the following error:
Exception raised in callable attribute "production_cost"; original exception was: 'function' object has no attribute 'getCheapest'
Is it even possible to do it as I am trying to do or is there some other way of achieving this?

first is a method; you didn't call it.
sub_article = Subarticle.objects.filter(parent_article=self).order_by('priority').first()
Note, an easier way of spelling this is:
sub_article = self.subarticles.all().order_by('priority').first()

Related

Django Rest Framework: optimize nester serializers performance

I have problem with my endpoint performance which returns around 40 items and response takes around 17 seconds.
I have model:
class GameTask(models.Model):
name= models.CharField()
description = RichTextUploadingField()
...
and another model like that:
class TaskLevel(models.Model):
master_task = models.ForeignKey(GameTask, related_name="sub_levels", on_delete-models.CASCADE)
sub_tasks = models.ManyToManyField(GameTask, related_name="master_levels")
...
So basicly I can have "normal" tasks, but when I create TaskLevel object I can add master_task as a task which gonna fasten other tasks added to sub_tasks field.
My serializers look like:
class TaskBaseSerializer(serializers.ModelSerializer):
fields created by serializers.SerializerMethodField()
...
class TaskLevelSerializer(serializers.ModelSerializer):
sub_tasks = serializers.SerializerMethodField()
class Meta:
model = TaskLevel
def get_sub_tasks(self, obj: TaskLevel):
sub_tasks = get_sub_tasks(level=obj, user=self.context["request"].user) # method from other module
return TaskBaseSerializer(sub_tasks, many=True, context=self.context).data
class TaskSerializer(TaskBaseSerializer):
levels_config = serializers.SerializerMethodField()
class Meta:
model = GameTask
def get_levels_config(self, obj: GameTask):
if is_mastertask(obj):
return dict(
sub_levels=TaskLevelSerializer(
obj.sub_levels.all().order_by("number"), many=True, context=self.context
).data,
progress=get_progress(
master_task=obj, user=self.context["request"].user
),
)
return None
When I tried to measure time it turned out that get_levels_config method takes around 0.25 seconds for one multilevel-task (which contain 7 subtasks). Is there any way to improve this performance? If any more detailed methods are needed I will add them
Your code might be suffering from N+1 problem. TaskSerializer.get_levels_config() performs database queries from obj.sub_levels.all().order_by("number").
What happens when serializing multiple instances like:
TaskSerializer(tasks, many=True)
each instance calls .get_levels_config()
You can use prefetch_related & selected_related(more explanation here).
You will have to manually check for prefetched objects since you are using SerializerMethodField. There's also the functions get_progress & get_sub_tasks which I assume does another query.
Here are some examples that can be used around your code:
Prefetching:
GameTask.objects.prefetch_related("sub_levels", "master_levels")
# Accessing deeper level
GameTask.objects.prefetch_related(
"sub_levels",
"master_levels",
"sub_levels__sub_tasks",
).select_related(
"master_levels__master_task",
)
Checking prefetch:
def get_sub_tasks(self, obj: TaskLevel):
if hasattr(obj, "_prefetched_objects_cache") and obj._prefetched_objects_cache.get("sub_tasks", None):
return obj._prefetched_objects_cache
return obj.sub_tasks.all()

use django_filters to filter for multiple arguments

I am using Relay, Django, Graphene Graphql.
I would like to use django_filters to filter for multiple arguments of type on accommodation. This is described in my schema file and atm looks like:
class AccommodationNode(DjangoObjectType) :
class Meta:
model = Accommodation
interfaces = (relay.Node,)
filter_fields = ['type']
This works perfectly if I pass a single string like: {"accommodationType": "apartment"}, but what if I want to filter for all accommodations that are apartments OR hotels? something like: {"accommodationType": ["apartment","hotel"]}
This is my model:
class Accommodation(models.Model):
ACCOMMODATION_TYPE_CHOICES = (
('apartment', 'Apartment'),
('host_family', 'Host Family'),
('residence', 'Residence'),
)
school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='accommodations')
type = models.CharField(
max_length=200,
choices=ACCOMMODATION_TYPE_CHOICES,
default='apartment'
)
def __str__(self):
return str(self.school) + " - " + self.type
Is there any way I can do this without writing custom filters as are suggested here? For only one filter field this is a great solution but I'll end up having around 50 throughout my application including linked objects...
Have a look at Django REST Framework Filters:
https://github.com/philipn/django-rest-framework-filters
It supports more than exact matches, like in, which you are looking for, but also exact, startswith, and many more, in the same style of Django's ORM. I use it frequently and have been impressed - it even integrates with DRF's web browseable API. Good luck!
like FlipperPA mentioned, I need to use 'in'. According to the django_filter docs:
‘in’ lookups return a filter derived from the CSV-based BaseInFilter.
and an example of BaseInFilter in the docs:
class NumberRangeFilter(BaseInFilter, NumberFilter):
pass
class F(FilterSet):
id__range = NumberRangeFilter(name='id', lookup_expr='range')
class Meta:
model = User
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
User.objects.create(username='carl')
# Range: User with IDs between 1 and 3.
f = F({'id__range': '1,3'})
assert len(f.qs) == 3
The answer to my question:
class AccommodationNode(DjangoObjectType) :
class Meta:
model = Accommodation
interfaces = (relay.Node,)
filter_fields = {
'type': ['in']
}
With the argument {"accommodationType": "apartment,hotel"} will work

Validate an inline foreign key field

I'm having problems trying to validate a form so that it only ever picks a distinct partner. The classes work like this:
class EmpployeeAdmin(admin.ModelAdmin):
#.......
class EmployeeRoles(models.Model):
partner = model.ForeignKey(Partner, relative_name='employee')
employee = model.ForeignKey(Employee, relative_name='partner')
class EmployeeRolesInline(admin.TabularInline):
model = EmployeeRoles
extra = 0
form = EmployeeRolesForm
Inside my forms.py I use the clean_partner function to try and validate that the current Employee only has one partner of the same name. They can have multiple EmployeeRoles objects with different partners, but they should only be assigned a partner once.
class EmployeeRolesForm(forms.ModelForm):
def clean_partner(self):
partner = self.cleaned_data.get('partner')
partner_ids=[int(p.partner_id) for p in self.instance.employee.partners.all()]
if self.instance.partner_id is not None:
return
else:
if partner.id in partner_ids:
raise forms.ValidationError("Partner already chosen")
This doesn't work, it gives me an error that self.instance.employee.partners.all() is empty. I think it has something to do with the form being unbounded and I'm not sure how to solve the problem. It's the same problem I had when I tried to filter the partner selection after every save so you only see the partners you don't have when you try to add a new employeerole. If anyone can suggest a way around the error I'd appreciate it.
You can just use unique_together in your model:
class EmployeeRoles(models.Model):
partner = model.ForeignKey(Partner, relative_name='employee')
employee = model.ForeignKey(Employee, relative_name='partner')
class Meta:
unique_together = (("partner", "employee"),)

Django model aggregation

I have a simple hierarchic model whit a Person and RunningScore as child.
this model store data about running score of many user, simplified something like:
class Person(models.Model):
firstName = models.CharField(max_length=200)
lastName = models.CharField(max_length=200)
class RunningScore(models.Model):
person = models.ForeignKey('Person', related_name="scores")
time = models.DecimalField(max_digits=6, decimal_places=2)
If I get a single Person it cames with all RunningScores associated to it, and this is standard behavior. My question is really simple: if I'd like to get a Person with only a RunningScore child (suppose the better result, aka min(time) ) how can I do?
I read the official Django documentation but have not found a
solution.
I am not 100% sure if I get what you mean, but maybe this will help:
from django.db.models import Min
Person.objects.annotate(min_running_time=Min('time'))
The queryset will fetch Person objects with min_running_time additional attribute.
You can also add a filter:
Person.objects.annotate(min_running_time=Min('time')).filter(firstName__startswith='foo')
Accessing the first object's min_running_time attribute:
first_person = Person.objects.annotate(min_running_score=Min('time'))[0]
print first_person.min_running_time
EDIT:
You can define a method or a property such as the following one to get the related object:
class Person(models.Model):
...
#property
def best_runner(self):
try:
return self.runningscore_set.order_by('time')[0]
except IndexError:
return None
If you want one RunningScore for only one Person you could use odering and limit your queryset to 1 object.
Something like this:
Person.runningscore_set.order_by('-time')[0]
Here is the doc on limiting querysets:
https://docs.djangoproject.com/en/1.3/topics/db/queries/#limiting-querysets

Django: construct a QuerySet inside a view?

I have models as follows:
class Place(models.Model):
name = models.CharField(max_length=300)
class Person(models.Model):
name = models.CharField(max_length=300)
class Manor(models.Model):
place = models.ManyToManyField(Place, related_name="place"))
lord = models.ManyToManyField(Person, related_name="lord")
overlord = models.ManyToManyField(Person, related_name="overlord")
I want to get all the Places attached with the relation 'lord' to a particular person, and then get the centre, using a GeoDjango method. This is as far as I've got:
person = get_object_or_404(Person, namesidx=namesidx)
manors = Manor.objects.filter(lord=person)
places = []
for manor in manors:
place_queryset = manor.place.all()
for place in place_queryset:
places.append(place)
if places.collect():
centre = places.collect().centroid
However, this gives me:
AttributeError at /name/208460/gamal-of-shottle/
'list' object has no attribute 'collect'
Can I either (a) do this in a more elegant way to get a QuerySet of places back directly, or (b) construct a QuerySet rather than a list in my view?
Thanks for your help!
The way you're doing this, places is a standard list, not a QuerySet, and collect is a method that only exists on GeoDjango QuerySets.
You should be able to do the whole query in one go by following the relations with the double-underscore syntax:
places = Place.objects.filter(manor__lord=person)
Note that your use of related_name="place" on the Manor.place field is very confusing - this is what sets the reverse attribute from Place back to Manor, so it should be called manors.