I have the following models in my django app:
from django.contrib.auth.models import User
class Poll(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User)
class Choice(models.Model):
poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
text = models.CharField(max_length=200)
class Vote(models.Model):
choice = models.ForeignKey(Choice)
user = models.ForeignKey(User)
For a given Poll, I need to display in the template a double-entry table like the following, listing all Users who have voted in this Poll and placing an 'x' for each Choice that the user has voted:
[choice1] [choice2] [choice3]
[user1] x
[user2] x x
[user3] x
[user4] x x
What would be the optimal way to implement this using the django ORM in order to minimize the database hits when populating the table?
Which object should be passed to the template in the context variable, in order conduct the logic in the view instead of the template?
The queries I know:
# get all votes for the given Poll (pk)
votes = Vote.objects.filter(choice__poll__pk=pk)
# get all users that has voted
usernames = votes.values('user__username')
# get the choices for the Poll
choices = Poll.objects.get(pk=pk).choice_set.all()
I was able to solve the problem with the following:
# yourapp/models.py
from django.utils.functional import cached_property
from django.db.models import Case, When, BooleanField
class Poll(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User)
#cached_property
def choices(self):
return self.choice_set.order_by('id')
#cached_property
def users_choices(self):
users = User.objects.filter(vote__choice__poll=self)
for choice in self.choices:
users = users.annotate(
**{str(choice.id): Case(When(vote__choice_id=choice.id, then=True), output_field=BooleanField())}
)
return users
For example in PostgreSQL users qs converted in such sql query:
SELECT
"yourapp_user"."id",
"yourapp_user"."username",
CASE WHEN "yourapp_vote"."choice_id" = 1
THEN TRUE
ELSE NULL END AS "1",
CASE WHEN "yourapp_vote"."choice_id" = 2
THEN TRUE
ELSE NULL END AS "2",
CASE WHEN "yourapp_vote"."choice_id" = 3
THEN TRUE
ELSE NULL END AS "3"
FROM "yourapp_user"
LEFT OUTER JOIN "yourapp_vote" ON ("yourapp_user"."id" = "yourapp_vote"."user_id")
LEFT OUTER JOIN "yourapp_choice" ON ("yourapp_vote"."choice_id" = "yourapp_choice"."id")
WHERE "yourapp_choice"."poll_id" = 1
View and template can look like this (you don't even need to pass context to template, everything will be taken from Poll model attributes):
# yourapp/views.py
class PollDetailView(DetailView):
model = Poll
template_name = 'user_choices.html'
# yourapp/templates/user_choices.html
{% extends 'base.html' %}{% load customtags %}
{% block content %}
<h1>{{ poll.title }}</h1>
<table border="1">
<tr>
<th>username</th>
{% for choice in poll.choices %}<th>choice{{ choice.id }}</th>{% endfor %}
</tr>
{% for user_choice in poll.users_choices %}
<tr>
<td>{{ user_choice.username }}</td>
{% for choice in poll.choices %}
<td>{% if user_choice|get_attr:choice.id %}+{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endblock %}
I had to add custom template filter to get attr by choice id (propably there is more elegant solution which i missed):
# yourapp/templatetags/customtags.py
from django import template
register = template.Library()
#register.filter(name='get_attr')
def get_attr(obj, attr_name):
return getattr(obj, str(attr_name), None)
Related
Template tag screenshot and possible solution options
First time, posting to stack-overflow. Please excuse if formatting is not ideal.
html
<tbody>
{% for list in todolist %}
<tr>
<td>
{{ list.name }}
</td>
<td>
{{ list.items.all|length }}
</td>
<td>
{% comment %}need this to be allCompleted[list.id - 1]
#allCompleted.0 works. allCompleted[0] or allCompleted['0'] does not.{% endcomment %}
{% if allCompleted == True %}
{{ allCompleted|getindex:list.id }}
Yes
{% else %}
No
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
Views.py:
class TodoListListView(ListView):
model = TodoList
context_object_name = "todolist"
template_name = "todos/list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
allCompletedList = []
for list in TodoList.objects.all():
allCompleted = True
for item in list.items.all():
if item.is_completed == False:
allCompleted = False
allCompletedList.append(allCompleted)
context['allCompleted'] = allCompletedList
print ('context: ', context)
return context
Models.py
class TodoList(models.Model):
name = models.CharField(max_length=100, blank=False)
created_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class TodoItem(models.Model):
task = models.CharField(max_length=100)
due_date = models.DateTimeField(null=True, blank=True)
is_completed = models.BooleanField(default=False)
list = models.ForeignKey("Todolist", related_name="items", on_delete=models.CASCADE)
def __str__(self):
return self.task
When printing context:
I get 'allCompleted': [False, True]
This is accurate as I have some items in housing chores not completed but I triple checked to make sure all my coding projects are completed.
As seen from the HTML screenshot, I need something like:
{{ allCompleted[list.id - 1] }} to match with the corresponding list in each row.
But it seems Django does not like that. I've tried many combos like allCompleted['list.id-1']. Strangely, allCompleted.0 = False but allCompleted[0] gets a parse error. I have also tried to create a custom template tag in my app/templatetag folder under a file I made (getindex.py)
from django import template
from todos.models import TodoItem, TodoList
register = template.Library()
def getindex(lst, idx):
return lst[idx]
register.filter(getindex)
For my template tag, I did {{ allCompleted|getindex:list.id-1 }} and it says getindex is not a valid filter so maybe I am registering it incorrectly?
If there is no way to access allCompleted[list.id - 1], I thought of other solutions explained in my HTMl screenshot.
Instead of using template tags for this purpose, it is best to give to the template data in the form it can easily use. The most efficient way to do this would be to leave the task to the database itself and write a query that will give you allCompleted as a column in the result. You can use Exists() subqueries to do this:
from django.db.models import Exists, OuterRef
class TodoListListView(ListView):
model = TodoList
context_object_name = "todolist"
template_name = "todos/list.html"
def get_queryset(self):
queryset = super().get_queryset()
subquery = TodoItem.objects.filter(
list=OuterRef('pk'),
is_completed=False
)
queryset = queryset.annotate(all_completed=~Exists(subquery))
return queryset
Then in your template you can simply write {{ list.all_completed }}:
<tbody>
{% for list in todolist %}
<tr>
<td>
{{ list.name }}
</td>
<td>
{{ list.items.all|length }}
</td>
<td>
{% if list.all_completed == True %}
Yes
{% else %}
No
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
The Priority model has three values, for each of them values I'm returning an inlineform which allows the user to set a score for each priority & then save with the Project.
This is what it currently looks like: Current view
My problem is: how can I automatically show all the priority values and allow the user to enter the score but not have to pick the Priority. Is it possible to show it like the image below?
What I'm trying to do.
Views.py
class ProjectCreateview(LoginRequiredMixin, CreateView):
model = Project
form_class = ProjectCreationForm
login_url = "/login/"
success_url = '/'
def get_context_data(self, **kwargs):
PriorityChildFormset = inlineformset_factory(
Project, ProjectPriority, fields=('project', 'priority', 'score'), can_delete=False, extra=Priority.objects.count(),
)
data = super().get_context_data(**kwargs)
if self.request.POST:
data['priorities'] = PriorityChildFormset(self.request.POST)
else:
data['priorities'] = PriorityChildFormset()
return data
def form_valid(self, form):
context = self.get_context_data()
prioritycriteria = context["priorities"]
form.instance.creator = self.request.user
self.object = form.save()
prioritycriteria.instance = self.object
if prioritycriteria.is_valid():
prioritycriteria.save()
return HttpResponseRedirect(self.get_success_url())
Models.py
class Priority(models.Model):
title = models.CharField(verbose_name="Priority Name", max_length=250)
def __str__(self):
return self.title
class Project(models.Model):
name = models.CharField(verbose_name="Name", max_length=100)
details = models.TextField(verbose_name="Details/Description", blank=False)
creator = models.ForeignKey(User, on_delete=models.CASCADE)
class ProjectPriority(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
priority = models.ForeignKey(Priority, on_delete=models.CASCADE)
score = models.CharField(max_length=1000, choices=priority_choices)
class Meta:
verbose_name = "Priority"
verbose_name_plural = "Priorities"
Template
<table class="table table-light">
<tbody>
{{ priorities.management_form }}
{% for priority in priorities.forms %}
<tr>
{% for field in priority.visible_fields %}
<td>
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
You can do this by using initial data with your formset (see the Django documentation here).
In your views.py code you can add this line to generate some initial values for the priority fields:
initial = [{'priority': priority} for priority in Priority.objects.all()]
And then pass it to your formset:
data['priorities'] = PriorityChildFormset(initial=initial)
Note that initial expects a list of the same length as the formset you created, which is defined by the extra parameter. This works because both of these parameters have been defined using the same base queryset (i.e. Priority.objects). If a filter were applied - it would need to apply in both places.
Changing how it displays
In addition, if you want to prevent the priority fields from being changed by the user using the dropdown menu, you can pass a widgets keyword argument to inlineformset_factory to set a disabled attribute on the <select> element that gets generated e.g.:
from django import forms
...
widgets={'priority': forms.Select(attrs={'disabled': True})}
If you want the field to show only text - this is more difficult. The string representation of the related object that we want is buried in the field choices that are used to generate each <option>. If you want to do this, you can dig out each field manually:
{% for priority in priorities.forms %}
<tr>
<td>
{{ priority.priority.errors.as_ul }}
{% for value, name in priority.priority.field.choices %}
{% if value == priority.priority.value %}
{{ name }}
{% endif %}
{% endfor %}
{{ priority.priority.as_hidden }}
</td>
<td>
{{ priority.score.errors.as_ul }}
{{ priority.score }}
</td>
</tr>
{% endfor %}
models.py
class Product(models.Model)
name = models.CharField()
class User(models.Model)
name = models.CharField()
class List(models.Model)
user = models.ForeignKey(User)
class Item(models.Model)
name = models.CharField()
list = models.ForeignKey(List)
user = models.ForeignKey(User)
product = models.ForeignKey(Product)
views.py
class ListView(View)
def get(self, request, pk)
list = List.objects.get(id=pk)
return render(request, "list.html", {"list" : list})
list.html
{% for item in list.item_set.all %}
{{ item.name }}
{{ item.user.name }} - ??
{{ item.product.name }} - ??
{% endfor %}
How get user.name и product.name?
I tried it:
{% item.user_set.first.name %} - does not work
{% for user in item.user_set.all %}{{ user.name }}{% endfor %} - does not work
Model method:
def get_user(self):
self.user_ser.get() #does not work
How do I solve this problem?
Since an Item has a ForeignKey to a Product object, you can access the related object with item.product, so you can render this with:
{{ item.product.name }}
{{ item.user.name }}
If you use the standard User module, it is {{ item.user.username }}. It is possible that this is empty, for example if the name of the related user is the empty string ''.
In order to retrieve the elements more efficient however, it might be better to perform a .prefetch_related(..) [Django-doc] to retrieve these items in bulk:
class ListView(View)
def get(self, request, pk)
list = List.objects.prefetch_related('item_set', 'item_set__product', 'item_set__user').get(id=pk)
return render(request, "list.html", {'list' : list})
From user : {{user.items}} # to access to the items objects
From item: {{item.user.name}} # to access to the user name field
class Item(models.Model)
name = models.CharField()
list = models.ForeignKey(List)
user = models.ForeignKey(User, related_name='items')
product = models.ForeignKey(Product)
It works!
My error was that I looked at the field names in the database, not in the file models.py. They are different.
Thank you for your help!
All the answers above are correct!
I'm stuck on my next step of showing the query from the database to the html template. I'm able to put some basic information, but I got stuck on the foreign key queries.
Here is sample of my code:
Here is my model:
class Player_Bios(models.Model):
my_id = models.SlugField(unique=True)
player_id = models.IntegerField(primary_key=True, max_length=50)
name = models.CharField(max_length=50)
last = models.CharField(max_length=50)
def __unicode__(self):
return self.player_id
class BatStat (models.Model):
player_id = models.ForeignKey('Player_Bios')
team_id = models.ForeignKey('Team')
bat_stat_id = models.CharField(max_length=50, unique=True)
sport_code = models.CharField(max_length=50, blank=True)
ab = models.IntegerField(max_length=50, null=True)
def __unicode__(self):
return self.bat_stat_id
My View:
def SpecificPLayer(request, playerslug):
player = Player_Bios.objects.get(player_id=playerslug) #this is the name, last and it is working fine
playerStat = BatStat.objects.filter(player_id=playerslug) #Here I'm calling the foreign key
print playerStat
context = {'player' : player, 'today' : date.today(), 'playerStat' : playerStat }
return render_to_response ('singleplayer.html', context, context_instance=RequestContext(request))
My HTML Template:
{% extends 'base.html' %}
{% block content %}
<div id="singleplayer">
<p>Name: {{ player.name|capfirst }}</p>
<p>Last Name: {{ player.last|capfirst }}</p>
</div>
{% endfor %}
{% endblock %}
Now when I do print playerStat, I get all of the BatStat from the player_id. In y case I get the following:
[<BatStat: 40539520011>, <BatStat: 40539520021>, <BatStat: 40539520031>]
I get the result that I want on the shell by doing the following:
playerStatID=BatStat.objects.filter(player_id='the player id here')
print playerStatID
[<BatStat: 40539520011>, <BatStat: 40539520021>, <BatStat: 40539520031>]
for i in playerStatID:
playerStat= BatStat.objects.get(bat_stat_id=i)
print BatStat.ab
200
So by doing that I can get the information that I need it, now how can I get that, but to put it on the template. I can't use a loop on the template to get a query, so I guess the loop has to be done on the view, but how. Thanks
Since you have a ForeignKey between BatStat and Player_Bios, you can use a batstat_set to get all related BatStats for the single player.
[Docs for _set here.][1]
So you would have your views.py as:
def SpecificPLayer(request, playerslug):
player = Player_Bios.objects.get(player_id=playerslug) #this is the name, last and it is working fine
batstats = player.batstat_set.all()
context = {'player' : player, 'today' : date.today(), 'batstats': batstats, }
return render_to_response ('singleplayer.html', context,context_instance=RequestContext(request))
and in your template:
{% for stat in batstats %}
{{ stat.ab }}
{% endfor %}
I'm trying to list all data on a page from table 'points_points' where user_id is the user logged in. However, the object is blank when displayed no data is listed. I 100% have data in the DB for the user. Why does nothing display? Can anyone spot any mistakes below?
models.py
class PointsManager(models.Manager):
def points_list(self,thisUser):
list = Points.objects.filter(user=thisUser)
return list
class Points (models.Model):
user = models.ForeignKey(User)
points = models.IntegerField(verbose_name=("Points"), default=0)
created = models.DateTimeField(("Created at"), auto_now_add=True)
updated = models.DateTimeField(verbose_name=("Updated at"), auto_now=True)
objects = PointsManager()
class Meta:
verbose_name = ('Point')
verbose_name_plural = ('Points')
views.py
#login_required
def history(request):
thisPoints = Points.objects.points_list(request.user)
context = {'points':thisPoints}
return render_to_response('points/history.html', context, context_instance=RequestContext(request))
template.py
<h1>|{{ points }}|</h1>
{% for item in points.Points_items.all %}
<tr>
<td>{{ item.points }}</td>
</tr>
{% endfor %}
Try this
def points_list(self,thisUser):
return super(PointsManager, self).get_query_set().filter(user=thisUser)
Your template doesn't looks right..
{% for item in points %}
<tr>
<td>{{ item.points }}</td>
</tr>
{% endfor %}