Creating a Hierarchy View in Django - django

I'm trying to create a hierarchy view in Django, but I'm struggling to make sense of how to use QuerySets effectively.
What I'm aiming for eventually is a html page that displays courses like this:
Main Course 1 --- Child Course 1
--- Child Course 2
Main Course 2 --- Child Course 3
--- Child Course 4
Each group of courses would be wrapped in a div and styled etc.
In my view.py file I have the following:
class HierarchyView(generic.ListView):
template_name = 'curriculum/hierarchy.html'
def get_queryset(self):
return Offering.objects.all()
def get_context_data(self, **kwargs):
context = super(HierarchyView, self).get_context_data(**kwargs)
context['main'] = self.get_queryset().filter(course_type='M')
context['sub'] = self.get_queryset().filter(parent_code__in=context['main'])
return context
The Offering model is set up so that parent_code is a self-referential foreign key (i.e. any course can be a child of any other), like this:
...
parent_code = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.SET_NULL)
...
And in my html template I have:
{% for mainoffering in main %}
<div>
<div>{{ mainoffering.course_name }}</div>
{% for offering in sub %}
<div>{{ offering.course_name }}</div>
{% endfor %}
</div>
{% endfor %}
What this results in, however, is that all child courses appear under all main courses, regardless of whether or not they are actually children of that course, which is obviously not what I'm after.
I'm still learning the ropes in Django, and I'm struggling to find anything that explains in plain English what I need to do. Please help!

I think you would need to change your template to match each of the child courses to their parent courses. Maybe something like:
{% for mainoffering in main %}
<div>
<div>{{ mainoffering.course_name }}</div>
{% for offering in sub %}
{% if offering.parent_code == mainoffering %}
<div>{{ offering.course_name }}</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}

The context['sub'] will return all of them, without any grouping, ordering etc. You can do couple of things to get the desired behavior.
You can do a prefetch related.
from django.db.models import Prefetch
offerings = Offering.objects.filter(course_type='M').prefetch_related(
Prefetch(
"courses_subset",
queryset=Offering.objects.filter(parent_code__in=offerings),
to_attr="sub"
)
)
for o in offerings:
print o.sub
You can actually make this a method in your model and create a template tag (i'd most likely use this).
method in your Offering model
def get_child_courses(self):
child_courses = Offerings.objects.filter(parent_code=self.id)
return child_courses
template tag
#register.simple_tag
def get_child_courses(course):
return course.get_child_courses()
In your template:
{% for mainoffering in main %}
<div>
<div>{{ mainoffering.course_name }}</div>
{% for offering in mainoffering|get_child_course %}
<div>{{ offering.course_name }}</div>
{% endfor %}
</div>
{% endfor %}
You can group them in your template as suggested by accraze. I'd personally go for the second option

Related

How to filter a ManyToMany field from django template?

I have a model named Universities in which there is a ManyToMany field named bookmarks associated with User model. In template file, I have looped through all the universities {% for university in universities %} and I am trying to show a bookmark icon based on the logged in user has bookmarked that specific university or not. However, it seems like I can not filter the query in template directly. How do I do that?
Don't name your models with plural names. "universities" should be "university" because every instance of the model represents a single university.
You can access one to many and many to many relationships in the templates. How you do that depends if you have assigned a related_name to the relationship.
Let me show you an example:
class University(models.Model):
name = models.CharField(max_length=50)
bookmarks = models.ManyToManyField(Bookmark, on_delete="models.CASCADE", related_name="universities")
Then you pass a list of all university models to the template:
class MyView(View):
def get(self, request):
context = { 'universities' : University.objects.all() }
return render(request, "mytemplate.html", context)
And finally you access all that you need from the template. This part is slightly tricky but not too much.
{% for university in universities %}
{% for bookmark in university.bookmarks.all %}
{{ bookmark }}
{% endfor %}
{% endfor %}
If you were to instead pass to the template a list of Bookmark instances, then you would have used the related_name stated in our example.
So like this:
{% for bookmark in bookmarks %}
{% for university in bookmark.universities.all %}
{{ university }}
{% endfor %}
{% endfor %}
If you didn't specify a related_name, then you can access the same data using the _set convention:
{% for bookmark in bookmarks %}
{% for university in bookmark.university_set.all %}
{{ university }}
{% endfor %}
{% endfor %}

Group by location and group in template

I try to display data from db and place them under correct title.
models.py
class Game(models.Model):
location= models.ForeignKey('location', on_delete=models.CASCADE')
group = models.IntegerField()
firstname = models.CharField()
surname = models.CharField()
views.py
class group(generic.CreateView):
template_name = 'group.html'
def get(self, request, *args, **kwargs):
game = Game.objects.filter(location__type__pk=kwargs['pk']).order_by('location__pk', 'group')
context = {
'game': game,
}
return render(request, self.template_name, context)
Lets say that the 'group' can be like A, B, C etc.
In the template I want to display it like
Location Foo
Group A
Jonas Andersson
Lisa Silverspoon
Group B
Sven Bohlin
Göran Lantz
Location Bar
Group A
Mia Milakovic
Lars Larsson
Group B
Anna Annasdotter
I have tried with so many variants of for-loops without any success.
Is it possible? Can you do this?
You can use template filter regroup, as in,
{% regroup games by location as games_list %}
{% for location, games in country_list %}
{{ location }}
{% for game in games %}
{{ game.group }}
...
{% endfor %}
{% endfor %}
Note: games here is a list, which is why you need to loop over it again.
Hope this helps!
The builtin regroup tag should enable you to do this, but you'll need to use it twice in order to achieve the nested grouping that you're looking for.
{% regroup game by location as games_loc %} <!-- Group by "location" -->
{% for location, l_games in games_loc %}
<div>{{ location }}</div>
{% regroup l_games by group as games_grp %} <!-- Group by "group" -->
{% for group, g_games in games_grp %}
<div>{{ group }}</div>
{% for g in g_games %} <!-- Display games in group -->
<div>{{ g }}</div>
{% endfor %}
{% endfor %}
{% endfor %}

Best way to show data from multiple tables

I have a recurring problem that I can't seem to solve adequately. My site is akin to a job site, where people can post jobs (and details within), and other people can bookmark tthem. Each job can obviously be bookmarked by more than one viewer.
So here's the model.py:
class Job(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=150)
description = models.CharField(max_length=5000)
class Bookmark(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
job = models.ForeignKey(Job, on_delete=models.CASCADE)
My intention is to display all jobs in the homepage, that means multiple jobs from multiple users. Other users can click on a little bookmark icon to bookmark it, or to unbookmark it. Like so:
Here's the view.py:
def index(request):
context = {}
populateContext(request, context)
jobs = Job.objects.all().order_by('-id')
context = {'jobs': jobs}
return render(request, 'templates/index.html', context)
Pretty simple I think, which is my intention. Here's how the template looks:
{% for job in jobs %}
<div>
{{ job.title }}<br>
{{ job.description }}<br>
by {{ job.user.username }}
</div>
{% endfor %}
My question is, how do I display the bookmark state specific to each user? Here's my current solution:
{% for job in jobs %}
<div>
{{ job.title }}<br>
{{ job.description }}<br>
by {{ job.user.username }}<br>
{% for watch in job.bookmark_set.all %}
{% if watch.user_id = request.user.id %}
You have bookmarked this! <a>Unbookmark!</a>
{% endif %}
{% endfor %}
<a>Bookmark</a>
</div>
{% endfor %}
The "Unbookmark!" link will be positioned over the "Bookmark" link by the way. The solution above works that way, because the bookmark table will contain zero or more bookmarks for a particular job, but under multiple users. I would handle this in the view.py, by filtering just jobs that the logged-in user has made. But I can't filter by which job specifically, because the index.html will be displaying them all. So for example, I could spit this out...:
bookmarked = Bookmark.objects.all().filter(user_id=request.user.id)
...and that would list out all bookmarks on different jobs that the logged-in user had made. I still need to filter that in the template, so that each project matches with each bookmark, and I understand this isn't possible.
Anyway, I think this is pretty inefficient. So I was wondering if there was an easier way to handle this? Preferably so that it works this way:
{% for job in jobs %}
<div>
{{ job.title }}<br>
{{ job.description }}<br>
by {{ job.user.username }}<br>
{% if job.id = bookmark.job.id %}
You have bookmarked this! <a>Unbookmark!</a>
{% else %}
<a>Bookmark</a>
{% endif %}
</div>
{% endfor %}
Thank you!
You can use conditional expressions to annotate your jobs with this information. In the view:
from django.db.models import Case, IntegerField, Sum, When
jobs = Job.objects.annotate(
is_bookmarked=Sum(Case(
When(bookmark__user=request.user, then=1),
default=0, output_field=IntegerField()
))).order_by('-id')
Each job now has an is_bookmarked property which is either 1 (the user has bookmarked the job) or 0. In your template:
{% for job in jobs %}
<div>
{% if job.is_bookmarked %}
You have bookmarked this! <a>Unbookmark!</a>
{% else %}
<a>Bookmark</a>
{% endif %}
</div>
{% endfor %}
Just for completeness, the other approach you had in mind would also work (although less efficient than the one above). In the view:
jobs = Job.objects.all().order_by('-id')
# Get a list of all Job IDs bookmarked by this user
user_bookmarks = Bookmark.objects.filter(user_id=request.user.id)\
.values_list('job__id', flat=True)
In the template:
{% for job in jobs %}
<div>
{% if job.id in user_bookmarks %}
You have bookmarked this! <a>Unbookmark!</a>
{% else %}
<a>Bookmark</a>
{% endif %}
</div>
{% endfor %}
Both these approaches are doing pretty much the same logic - the difference being that the first one does this at database level which is generally more efficient.

Need some advice for duplicated queries

I have a lot of duplicated queries (in django debug toolbar) when i load my menu tabs, im sure i can optimize this but don't find the good way.
Models :
class Categorie(models.Model):
name = models.CharField(max_length=30)
visible = models.BooleanField(default = False)
def __str__(self):
return self.nom
def getscateg(self):
return self.souscategorie_set.all().filter(visible = True)
class SousCategorie(models.Model):
name = models.CharField(max_length=30)
visible = models.BooleanField(default = False)
categorie = models.ForeignKey('Categorie')
def __str__(self):
return self.name
def gettheme(self):
return self.theme_set.all().filter(visible = True)
class Theme(models.Model):
name = models.CharField(max_length=100)
visible = models.BooleanField(default = False)
souscategorie = models.ForeignKey('SousCategorie')
def __str__(self):
return self.name
Views :
def page(request):
categs = Categorie.objects.filter(visible = True)
return render(request, 'page.html', locals())
Templates :
{% for categ in categs %}
<li>
{{categ.name}}
<ul>
{% for scateg in categ.getscateg %}
<li>
{{scateg.name}}
<ul>
{% for theme in scateg.gettheme %}
<li>{{ theme.name }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
I have look at prefetch_related but only work if i want load Categorie from SousCategorie and SousCategorie from Theme, so if i understand i need the contrary of this...
Solved !
If it can help :
from .models import Categorie, SousCategorie, Theme, SousTheme
from django.db.models import Prefetch
pf_souscategorie = Prefetch('souscategorie_set', SousCategorie.objects.filter(visible=True))
pf_theme = Prefetch('souscategorie_set__theme_set', Theme.objects.filter(visible=True))
pf_soustheme = Prefetch('souscategorie_set__theme_set__soustheme_set', SousTheme.objects.filter(visible=True))
categs = Categorie.objects.filter(visible=True).prefetch_related(pf_souscategorie, pf_theme, pf_soustheme)
and call like this in the template :
{% with currscat=categ.souscategorie_set.all %}
{% with currth=souscateg.theme_set.all %}
{% with currsth=theme.soustheme_set.all %}
Bye
In Django every time you evaluate a new queryset a query is executed, so you need to reduce the number of queryset being used. Here is what is happening:
You create a queryset Categorie.objects.filter(visible=True) and it is passed into the view layer, there the first query is executed at the this tag {% for categ in categs %}
Inside the loop, for every category you're calling a method categ.getscateg which returns a new queryset return self.souscategorie_set.all().filter(visible = True), this queryset will be executed at the second loop in your template {% for scateg in categ.getscateg %}
The same thing happens with {% for theme in scateg.gettheme %}
Using prefetch_related was the right move, try something like (didn't test it):
Categorie.objects.filter(visible=True, souscategorie_set__visible=True, souscategorie_set__theme_set__visible=True).prefetch_related('souscategorie_set__theme_set')
prefetch_related works by running a first query to load the categories that satisfy your current filter, then it executed a second query to load all subcategories and so on.
In other cases you can use select_related, but that only works when a single query can be used, as an example, it would work if you needed the category and subcategory of a theme, like:
Theme.objects.filter(pk=1).select_related('souscategorie__categorie')
The difference here is that Theme is the one that has the ForeignKey, so it has only one subcategory, and it can be loaded with a single join, that means you can use select_related only when your queryset is from the Model that points and I think that it also works with OneToOneField.
I have reduce my querysets by 2 by using "with"
It's a good point but i always got a lot of duplicate (like 44 duplicate for 51 queries), for example i hit my database when i do that :
{% for categ in categs %}
{% with currscat=categ.getscateg %}
<li class = "{% if currscat %} has_menu {% endif %}"> {{categ.name}} # This line hit database
{% if currscat %} # This line hit database
<ul class = "menu-1"> ... </ul>
{% endif %}
</li>
{% endwith %}
{% endfor %}
I try to use prefetch_related like this :
categs = Categorie.objects.filter(visible=True).prefetch_related('souscategorie_set')
but thats just add query to database, don't reduce...
Some advice?
Thanks

django filter a regroup within a forloop

I have a model called Subtopic. One of my templates runs a forloop on an object, returning a different field for each cell of a table row.
Two of the table cells look up a field which is a ManytoMany foreign key, both to the same foreign model, Resource. I want each to display different results, based on the value of a boolean field within the Resource model.
What you see below is currently working fine, but doesn't attempt to filter by the boolean field.
models.py:
class ICTResourceManager(models.Manager):
def get_query_set(self):
return super(ICTResourceManager, self).get_query_set().filter('is_ict': True)
class NonICTResourceManager(models.Manager):
def get_query_set(self):
return super(NonICTResourceManager, self).get_query_set().filter('is_ict': False)
class Resource(models.Model):
subtopics = models.ManyToManyField(Subtopic)
external_site = models.ForeignKey(ExternalSite)
link_address = models.URLField(max_length=200, unique=True, verify_exists=False)
requires_login = models.BooleanField()
is_ict = models.BooleanField()
flags = models.ManyToManyField(Flag, blank=True)
comment = models.TextField()
def __unicode__(self):
return u'%s %s' % (self.external_site, self.link_address)
objects = models.Manager()
ict_objects = ICTResourceManager()
nonict_objects = NonICTResourceManager()
class Meta:
ordering = ['external_site', 'link_address']
views.py:
def view_ks5topic(request, modulecode, topicshortname):
listofsubtopics = Subtopic.objects.filter(topic__module__code__iexact = modulecode, topic__shortname__iexact = topicshortname)
themodule = Module.objects.get(code__iexact = modulecode)
thetopic = Topic.objects.get(module__code__iexact = modulecode, shortname__iexact = topicshortname)
return render_to_response('topic_page.html', locals())
My template:
{% for whatever in listofsubtopics %}
<tr>
<td>
{{ whatever.objective_html|safe }}
<p>
{% if request.user.is_authenticated %}
{% with 'objective' as column %}
{% include "edit_text.html" %}
{% endwith %}
{% else %}
{% endif %}
</td>
<td>
{% regroup whatever.resource_set.all by external_site.name as resource_list %}
{% for external_site in resource_list %}
<h4>{{ external_site.grouper }}</h4>
<ul>
{% for item in external_site.list %}
<li>{{ item.comment }}</li>
{% endfor %}
</ul>
{% endfor %}
</td>
</tr>
{% endfor %}
As you can see, I've added extra managers to the model to do the filtering for me, but when I replace the appropriate lines in the template, I just get blanks. I have tried: for external_site.ict_objects in resource_list and for item.ict_objects in resource_list and <a href="{{ item.ict_objects.link_address }}">. If this were in the view I could probably do the filter just by .filter('is_ict': True), but with this being inside a forloop I don't know where to do the filtering.
I also tried writing regroup whatever.resource_set.filter('is_ict': True) in the template, but the syntax for regrouping seems to use resource_set.all rather than resource_set.all() (and I don't know why) so the filter text doesn't work here.
Turns out it was possible to do it using a custom template filter. The original efforts to filter within the template weren't working, given that as is well documented the template language is not a fully-fledged python environment. My original question remains open for anyone who knows an alternative method that more directly addresses the question I was originally asking, but here's how I did it:
myapp_extras.py:
from django import template
register = template.Library()
def ict(value, arg):
"filters on whether the is_ict Boolean is true"
return value.filter(is_ict=arg)
register.filter('ict', ict)
My template, note the use of the custom filter in line 2:
<td>
{% regroup whatever.resource_set.all|ict:1 by external_site.name as resource_list %}
{% for external_site in resource_list %}
<h4>{{ external_site.grouper }}</h4>
<ul>
{% for item in external_site.list %}
<li>{{ item.comment }}</li>
{% endfor %}
</ul>
{% endfor %}
</td>
After this I was able to remove the additional custom managers from the model.
One further question, when filtering for the boolean is_ict field, I found that I had to use filter(is_ict=1) and filter(is_ict=0). Is that the only way to refer to a True or False value?