Django nested prefetch_related - python

I'm using nested prefetching to link all my objects with each other and then pushing them to a template to show them.
My general setup: I have frameworks, categories and controls. A control is linked to a category, a category is linked to a framework. A category can also have a supercategory, in which case the category has a call to another category object. Every control also has an initial score, current score and a target score.
What I want to do is for every framework, for every supercategory, for every subcategory show every control with their scores.
Example:
Framework A
Super Category 1
Subcategory 1
Control 1
Initial
Current
Target
Control 2
...
Subcategory 2
Control 3
...
Super Category 2
Subcategory 3
Control 4
...
Framework B
...
I've already managed to do this by using a lot of nested prefetches.
My models:
# models.py
#==============[FRAMEWORKS]==============#
class Framework(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(null=True)
def __str__(self):
return self.name.replace(' ', '_')
class FrameworkCat(models.Model):
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=10,
null=True,
blank=True,
verbose_name="optional identifier")
framework = models.ForeignKey(Framework,
on_delete=models.CASCADE,
verbose_name="related framework")
superCat = models.ForeignKey('self',
on_delete=models.CASCADE,
verbose_name="super category",
blank=True,
null=True)
hexColor = models.CharField(max_length=10,
null=True,
blank=True,
verbose_name="hex color")
def __str__(self):
return self.name
#==============[CONTROLS]==============#
class Control(models.Model):
title = models.CharField(max_length=250)
description = models.TextField()
framework = models.ForeignKey(Framework,
on_delete=models.CASCADE,
verbose_name="related framework")
category = models.ForeignKey(FrameworkCat,
on_delete=models.CASCADE,
verbose_name="related category",
null=True)
def __str__(self):
return self.title
class ProjectScore(models.Model):
project = models.ForeignKey(Project,
on_delete=models.CASCADE,
verbose_name="related project")
control = models.OneToOneField(Control,
on_delete=models.CASCADE,
verbose_name="related control")
framework = models.ForeignKey(Framework,
on_delete=models.CASCADE,
verbose_name="related framework")
score = models.IntegerField(default=-1)
current_score = models.IntegerField(default=-1)
target = models.IntegerField(default=-1)
def __str__(self):
return "Project Score"
My (testing) view:
# views.py
def testing_models(request):
def_data = default_data(request)
frameworks = Framework.objects.prefetch_related(Prefetch('frameworkcat_set',
queryset=FrameworkCat.objects.prefetch_related(Prefetch('frameworkcat_set',
queryset=FrameworkCat.objects.prefetch_related(Prefetch('control_set',
queryset=Control.objects.select_related('projectscore').order_by('id'),
to_attr='controls')).exclude(superCat=None),
to_attr='categories')).filter(superCat=None),
to_attr='supercategories')).all()
def_data['frameworks'] = frameworks
return render(request, 'testing_models.html', def_data)
And my template to show everything:
# templates/testing_models.html
{% for framework in frameworks %}
{% for supercategory in framework.supercategories %}
<p><strong>{{ supercategory.name }}</strong></p>
{% for subcategory in supercategory.categories %}
<p>{{ subcategory.name }}</p>
{% for control in subcategory.controls %}
<p>{{ control.title }}</p>
<p>{{ control.projectscore.score }}</p>
<p>{{ control.projectscore.current_score }}</p>
<p>{{ control.projectscore.target }}</p>
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}
This all works correctly by executing 6 queries. Now, my main question is: is there a better way to fetch these objects and link them than using all these nested prefetches? Is there some other kind of syntax that I could be using that makes it easier to read and easier to modify? I have many more things I would like to link but I'm afraid the python statement would become too big and my overview would be lost.
Much appreciated!

Related

Check if user had filled questionnaire before with Django

I'm working on a questionnaire and I made a page where user have a list of which questionnaires to fill and which did he filled before but I have stucked.
I like to check if the user filled a form/questionnaire before and if he didn't show him the questionnaire link.
My solutions doesn't work because it checks the db just if the user filled the questionnaire but if he did not (no row for him in the db) it shows a blank cell in my table.
(I don't know if exists query could be a solution but I can't made it work)
main.html
{% for i in oke_vezetoi %}
{% if i.vezetoi_ok == True %}
<td><button class="btn btn-sm btn-outline-info"> Kitöltöm</button>
<td><i class="fas fa-running fa-2x text-dark"></i></td>
{% else %}
<td class="text-success text-uppercase">Kitöltötted</button>
<td><i class="fas fa-check fa-2x text-success"></i></td>
{% endif %}
{% endfor %}
views.py
def main(request):
oke_vezetoi = Vezetoi.objects.filter(user_name=request.user)
oke_stressz = Stressz_teszt.objects.filter(user_name=request.user)
context = {
'oke_vezetoi': oke_vezetoi,
'oke_stressz': oke_stressz,
}
return render(request, 'stressz/main.html', context)
models.py
class Vezetoi(models.Model):
def __str__(self):
return str(self.user_name)
user_name = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
vezetoi_v01 = models.IntegerField( null=True)
vezetoi_v02 = models.IntegerField( null=True)
vezetoi_v03 = models.IntegerField( null=True)
vezetoi_v04 = models.IntegerField( null=True)
vezetoi_v05 = models.IntegerField( null=True)
vezetoi_v06 = models.IntegerField( null=True)
vezetoi_v07 = models.IntegerField( null=True)
vezetoi_v08 = models.IntegerField( null=True)
vezetoi_v09 = models.IntegerField( null=True)
vezetoi_v10 = models.IntegerField( null=True)
vezetoi_v11 = models.IntegerField( null=True)
vezetoi_v12 = models.IntegerField( null=True)
vezetoi_ok = models.BooleanField()
forms.py
class VezetoiForm(forms.ModelForm):
class Meta:
model = Vezetoi
fields = ['vezetoi_v01', 'vezetoi_v02', 'vezetoi_v03', 'vezetoi_v04', 'vezetoi_v05', 'vezetoi_v06', 'vezetoi_v07', 'vezetoi_v08', 'vezetoi_v09', 'vezetoi_v10', 'vezetoi_v11', 'vezetoi_v12', 'vezetoi_ok' ]
I'm not sure if I'm interpreting your question correctly, but it seems like there are a few things that you could optimise.
It sounds like there should only be one entry per user in the Vezetoi model.
If this is true, you should enforce ForeignKey(unique=True), and you don't need a for loop and you can use Vezetoi.objects.get() in your views.py
If this is not true, and there are multiple Vezetoi integers per user, you might want to have one Vezetoi object for each integer.
If the user hasn't submitted a questionnaire, then the Vezetoi model object will not exist.
Since the object doesn't exist, it won't appear in the oke_vezetoi queryset, so the object attribute i.vezetoi_ok will not be found in your loop (this is why your table row is blank).
Assuming the field vezetoi_ok is only intended to check for the existence of the questionnaire, it can only ever be True, so you can remove it from the model definition.
If these are not true, I'll need to amend the answer and I'll ask you to provide more information about what these models are tracking, the content of the Stressz_teszt model and urls.py as well as how the VezetoiForm is implemented.
So in the case that I've described, I'd do it like this with a class based view and the get_context_data method.
models.py
class Vezetoi(models.Model):
def __str__(self):
return str(self.user_name)
user_name = models.ForeignKey(User, on_delete=models.CASCADE, unique=True)
vezetoi_v01 = models.IntegerField(null=True)
...
# vezetoi_ok = models.BooleanField()
forms.py
class VezetoiForm(forms.ModelForm):
class Meta:
model = Vezetoi
fields = '__all__'
views.py
class MainView(TemplateView):
template_name = 'main.html'
def get_context_data(self, **kwargs):
context = super().get_context_data()
context['oke_vezetoi'] = Vezetoi.objects.get(user_name=request.user)
context['oke_stressz'] = Stressz_teszt.objects.get(user_name=request.user)
return context
main.html
{% extends 'base.html' %}
{% if oke_vezetoi %}
{# do not display link #}
{% else %}
{# display link #}
{% endif %}

Displaying objects by categories in Django

I'm building an ecommerce app and I'd like to make a section that shows some featured items by category. I'm using three sliders that display those items; Each slider is a featured category and each item in the slider is a featured item.
The problem is that I can't figure out how to assign the item to the proper slider. For example: I want to assign a JeansJacket to "Clothes and accesories" and display it. I tried this:
{% for cat in categories %}
<h1>{{ cat.cat_name }}</h1>
<!--(carousel code in between)-->
<div class="carousel-inner" role="listbox">
{% for item in featured_items %}
{% if item.Categoría in cat.cat_name %}
{{ item }}
{% endif %}
This is a simplified version of what I have, without the rest of the content. I just can't figure out how to iterate though the featured items and display them in the corresponding category.
Edit: This is in models.py:
class Categorías(models.Model):
cat_name = models.CharField(max_length=30)
Destacado = models.BooleanField()
class Meta:
ordering = ('cat_name',)
verbose_name = 'Categoría'
verbose_name_plural = 'Categorías'
def __str__(self):
return self.cat_name
class publicaciones(models.Model):
Título = models.CharField(max_length=30)
Descripción = models.TextField(max_length=200)
Precio = models.FloatField()
Fotos = models.ImageField()
Categoría = models.ForeignKey(Categorías, on_delete=models.CASCADE)
Promocionado = models.BooleanField()
class Meta:
verbose_name = 'Publicación'
verbose_name_plural = 'Publicaciones'
def __str__(self):
return self.Título
You can use prefetch_related and Prefetch with a custom query to get all related articles for each category.
categories = Categorías.objects.prefetch_related(Prefetch(
'publicaciones_set',
queryset=publicaciones.objects.filter(Promocionado=True),
to_attr='featured_items'
))
Now you can loop over each category and then loop over this prefetch. You don't need to create a separate featured_items queryset
for category in categories:
for featured_item in category.featured_items:
...
You can use apply this pattern to your template

Simplifying code with multiple loops and logic, into queryset aggregates

My models:
class customer(models.Model):
cstid = models.AutoField(primary_key=True, unique=True)
insurance_number = models.CharField(max_length=100, blank=True, null=True)
name = models.CharField(max_length=35)
ageyrs = models.IntegerField(blank=True)
class Admission(models.Model):
id = models.AutoField(primary_key=True, unique=True)
clinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
customer = models.ForeignKey(customer, on_delete=models.CASCADE)
diagnosis = models.CharField(max_length=2000, default='', blank=True)
date_admission = models.DateTimeField(default=timezone.now)
ward = models.ForeignKey(Ward, on_delete=models.CASCADE)
bed = models.ForeignKey(Bed, on_delete=models.CASCADE)
discharged = models.BooleanField(default=False)
ip_number = models.IntegerField(blank=True)
ip_prefix = models.CharField(max_length=20, default='', blank=True)
My objective: Set a variable to a query filter, adding a property, 'is_admitted' to the queryset, so that I can pass this query set to the template and use the property while displaying data.
Code:
def is_admitted(cust):
admission = Admission.objects.filter(customer=cust, discharged=False)
admission_results = len(admission)
if admission_results > 0:
return True
return False
my_q = or_q_if_truthfull(
cstid=HospitalID,
name__lower__contains=name.lower() if name else None,
ageyrs=ageyrs if ageyrs.isdigit() else None,
agemnths=agemnths if agemnths.isdigit() else None,
mobile__contains=mobile if mobile else None,
alternate__contains=alternate if alternate else None,
email__lower__contains=email.lower() if email else None,
address__lower__contains=address.lower() if address else None,
city__lower__contains=city.lower() if city else None
)
ORSearchResult = customer.objects.filter(my_q, linkedclinic=clinicobj)
cust_set = []
cust_admission_status = []
for cust in ORSearchResult:
cust_set.append(cust)
cust_admission_status.append(is_admitted(cust))
print(f"Customer name: {cust.name} Admission status: {is_admitted(cust)}")
cust_templ_set = zip(cust_set, cust_admission_status)
And in template, I will do:
{% for cust, status in cust_templ_set %}
{{ cust.name }} {{ status }}
{% endfor %}
I want to understand how I can convert my above code by generating an aggregate over the queryset, so that I can use a property of the query, and change the template code to the following, and avoid the loop in the view, and the zip. So that the template code becomes:
{% for cust in customers %}
{{ cust.name }} {{ cust.is_admitted }}
{% endfor %}
I am not sure whether I am making complete sense, and can explain further.
I'm not sure I understood you right, perhaps you might want this:
cust = customer.objects.filter(my_q, linkedclinic=clinicobj)
is_admitted_sub_q = Admission.objects.filter(customer=OuterRef('pk'), discharged=False)
cust_templ_set = cust.annotate(is_admitted=Exists(is_admitted_sub_q), )
this will return a list of customers with additional field is_admitted which will be True if there exists at least one linked (to this customer) record in Admission.
OuterRef, Exists
One option could be to use conditional-expressions together with annotate(). It could look like this:
from django.db import models
qs = Customer.objects.filter(...) # your filter conditions go here
# now add a field to the resulting queryset
qs = qs.annotate(
active_admissions=models.Count(
models.Case(
models.When(admission__discharged=False, then=1),
output_field=models.IntegerField())))
Now each object in the queryset will have an additional attribute called active_admissions which will contain the number of active admissions.
This could be used in the template like this:
{% for cust in qs %}
{{ cust.name }} {{ cust.active_admissions }}
{% endfor %}
Maybe you need to modify the subquery to fit your specific needs. Does that help?

Django dealing with a model fields

I'm new to Django and I'm trying to learn as I go. And I've ended up in a situation where I can't figure out what is the best way forward.
snippet from models.py:
class ProjectMeta(models.Model):
project = models.ForeignKey(Project)
architect = models.CharField(max_length=200)
landscape = models.CharField(max_length=100, blank=True)
engineer = models.CharField(max_length=200, blank=True)
client = models.CharField(max_length=100)
consultant = models.CharField(max_length=100, blank=True)
size = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
location = models.CharField(max_length=200)
date = models.DateField()
STATUS = (
('CP', 'Competition'),
('UC', 'Under construction'),
('CO', 'Completed'),
)
status = models.CharField(max_length=2, choices=STATUS, default=1)
And this is the view:
class ProjectDetailView(DetailView):
model = Project
def get_context_data(self, **kwargs):
context = super(ProjectDetailView, self).get_context_data(**kwargs)
context['projectmeta_list'] = ProjectMeta.objects.all()
return context
But if I want to output ProjectMeta in the template I could iterate over projectmeta_list.
{% for metadata in projectmeta_list %}
<p>Architect: {{ metadata.architect }}</p>
{% endfor %}
But this require alot of repeating myself, and I wont work. Because lets say the architect field is empty, I would get Archiect: printed to the page. Is there a built-in way of converting a model into a dict or list, so I can iterate over it and only print out fields that aren't empty to the page?
I've been looking at get_fields(), would that work? https://docs.djangoproject.com/en/1.10/ref/models/meta/#retrieving-all-field-instances-of-a-model
I tried this in the shell, threw me and AttributeError:
>>> from projects.models import *
>>> Project._projectmeta.get_fields()
You should try wrapping the <p>Architect: {{ metadata.architect }}</p> piece in a conditional {% if metadata.architect != '' %} or some condition to that effect.
Try with another ProjectMeta model. Take a look at this one.
class ProjectMeta(models.Model):
project = models.ForeignKey(Project)
name = models.CharField(max_length=50)
value = models.TextField()
And this query should work. myproject.projectmeta_set.filter(name="status")
You can use built-in default or default_if_none template filters to show a default value if it is None or empty string.
{% for metadata in projectmeta_list %}
<p>Architect: {{ metadata.architect|default:"-" }}</p>
{% endfor %}
Check this for more details.

Access fields in Django

The problem is, when i'm trying to compare two properties which are the same when we check them in shell, but the condition is not complete and i have no idea why. I mean this condition: {% if c.author = member.name %}
views:
cvs = Cv.objects.all()
cv = Cv.objects.filter(author = request.user)
per = Person.objects.all()
gr = Group.objects.filter(members__name=request.user)
for c in cvs:
print c.author
mem = Membership.objects.all()
form = GroupForm()
context = {
'gr': gr,
'per':per,
'mem':mem,
'form': form,
'cvs':cvs,
'cv':cv,
}
return render(request, 'groups.html', context)
models.py:
class Cv(models.Model):
author = models.ForeignKey('auth.User')
name = models.CharField(max_length=25, null = True)
surname = models.CharField(max_length=25, null = True)
address = models.CharField(max_length=100, blank=True)
telephone = models.IntegerField()
birth_date = models.DateField(blank=True, null=True)
email = models.EmailField(max_length=50, null=True)
skills = models.TextField(null=True)
specialization = models.CharField(max_length=30, blank=True, null=True)
interests = models.TextField(blank=True, null=True)
summary = models.TextField(blank=True, null=True)
thumbnail = models.FileField(upload_to=get_upload_file_name, blank=True)
#property
def age(self):
return int((datetime.datetime.now().date() - self.birth_date).days / 365.25 )
def zapisz(self):
self.save()
def __str__(self):
return self.surname
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self): # __unicode__ on Python 2
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self): # __unicode__ on Python 2
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
leader = models.BooleanField(default=False)
group = models.ForeignKey(Group)
groups.html:
{% block profile %}
{% for g in gr %}
<div class="jumbotron">
<p><b>GROUP:</b> {{g.name}}</p>
{% for c in cvs %}
{% for member in g.members.all %}
{% if c.author = member.name %}
{{member.name}}
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% endfor %}
{% endblock %}
Thanks for answer!
Firstly, please give your variables descriptive names. "c" and "gr" are impossible to understand.
Secondly, you are not comparing the right things at all. c.author is an instance of User; member is an instance of Person and member.name is a string. Comparing a User instance with a string will always fail.
Finally, this whole thing is horribly inefficient - and probably totally unnecessary. Three nested for loops means a huge number of iterations. If you could explain what the output needs to be, then we can almost certainly come up with a better way of doing it.
To test for equality in a Django template you need to use the equality operator == as follows:
{% if c.author == member.name %}
{{member.name}}
{% endif %}
You should compare c.name with member.name or c.author.first_name with member.name. So both of the variable to be strings.

Categories

Resources