Django: Complex Formset with multiple foreignKeys - django

I'm working on a project where users can vote on collections of books. My models look like this:
class Collection(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=31)
class Book(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
collection = models.ForeignKey(Collection)
vote_choices = ((-1,'Bad'),
(0, 'Not good, Not bad'),
(1,'Good'))
class Vote(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, related_name='votes')
book = models.ForeignKey(Book)
vote = models.IntegerField(choices=vote_choices)
I need to display a view for the users where they can see all the books in a given collection and vote for those books at once, since this is much more user-friendly than looping over everybook to vote. So I need a formset where every form will be: a book and its vote.
Initially I thought this was a good opportunity to use formsets (I have never used them before, but I'm used to work with forms, modelforms), but I'm running into difficulties on how to create this formset.
I would like to use generic class based views, since, in theory, I could easily create/update all votes easily, but I can't figure out how to do this.
Right now, I don't have a direct relation between the Collection model and the User (from django.contrib.auth.model) model, but I don't mind adding one if this makes things easier. I don't really need one, since every User is linked to a Book through a Vote and every Book is linked to a Collection.
I'm not sure whether I should use formsets or just regular modelforms.
How would you do it? Thanks in advance for any help.

I would recommend not to use django forms for this case. There are some more convenient ways.
First way. Using pure jQuery
Extend your book class:
class Book(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
collection = models.ForeignKey(Collection, related_name='books')
def get_vote(self, user):
vote = Vote.objects.filter(book=self, user=user)
if vote:
return vote.vote
else:
return ''
Your views (you can rewrite it on CBV if you want):
def list_collection(request, collection_id):
collection = Collection.objects.get(pk=id)
user = request.user
books = []
for item in collection.books.all():
book = {
'id': item.pk,
'name': item.name,
'vote': item.get_vote(user)
}
books.append(book)
return render_to_response('colleciton.html', {'books': books})
def set_vote(request):
id = request.GET('id')
vote = int(request.GET('vote'))
user = request.user
book = Book.objects.get(pk=id)
vote = Vote()
vote.user = user
vote.vote = vote
vote.book = book
vote.save()
return HttpResponse()
Your urls:
url(r'^list-collection/', 'list_collection', name='list_collection'),
url(r'^set-vote/', 'set_vote', name='set_vote'),
Your django template:
{% extends 'base.html' %}
{% block content %}
{% for book in collection.books.all %}
<div class="book" data-id="{{ book.id }}">
<div class="book-name">{{ book.name]}</div>
<div class="book-vote" {% if not book.get_vote %}style="display:none"{% endif %}>book.get_vote</div>
<div class="voting" {% if book.get_vote %}style="display:none"{% endif %}>
<button class="vote-up"></button>
<button class="vote-down"></button>
</div>
</div>
{% endfor %}
{% endblock %}
And the javascript is like
$(document).ready(function(){
$('.vote-up, .vote-down').on('click', function(e){
var id, vote, t, book_el;
e.preventDefault();
t = $(e.currentTarget);
book_el = t.parents().parents()
id = book_el.attr('data-id');
if t.hasClass('vote-up'){
vote = 1;
} else {
vote = -1;
}
book_el.find('.book-vote').show();
book_el.find('.book-vote').text(vote);
book_el.find('voting').hide();
$.get("/set-vote", {vote: vote, id: id});
});
});
Second way. Using javascript frameworks like Backbone.js
It makes the whole process much easier. Especially if you plan to add some other features to your app.
From the other hand Backbone requires some time to learn it.
Read the docs, look at the tutorial app TodoMVC or other tutorials.
Maybe you'll find it more appropriate for your task.

Related

Django Templating Language objects comparison

I'm fairly new to Django. I have a database with Events. I want to display the name of the Organisation where org_id is the foreign key in the events model. My approach was to load in all the objects and then do some iterating through them but I get no output on the website. I feel like its got to do something with the templating language
models.py
class Organisation(models.Model):
org_id=AutoSlugField(unique=True)
name = models.CharField(max_length=200)
email=models.EmailField(max_length = 250)
class Event(models.Model):
event_id = AutoSlugField(unique=True)
name = models.CharField(max_length=100)
date = models.DateField()
event_category = models.CharField(max_length=50)
duration= models.IntegerField()
org_id = models.ForeignKey(Organisation,on_delete=models.CASCADE)
maxparticipants= models.IntegerField()
teacher_id=models.ForeignKey(User,on_delete=models.CASCADE)
relevant snippet from event_details.html
<h3>Hosting Body</h3>
{% for org in orgs%}
{%if org.org_id == event.org_id %}
<p>{{org.name}}</p>
{%endif%}
{%endfor%}
<h3>Date</h3>
<p>{{event.date}}</p>
views.py
def event_details(request,pk):
event=Event.objects.get(event_id=pk)
orgs=Organisation.objects.all()
context={'event':event,'orgs':orgs}
return render(request,'event_details.html',context)
You can render the relevant organization with:
<h3>Hosting Body</h3>
<p>{{ event.org_id.name }}</p>
<h3>Date</h3>
<p>{{ event.date }}</p>
You can boost efficiency by fetching the Event and Organisation data in the same query with:
from django.shortcuts import get_object_or_404
def event_details(request, pk):
event = get_object_or_404(Event.objects.select_related('org_id'), event_id=pk)
context = {'event':event}
Note: It is often better to use get_object_or_404(…) [Django-doc],
then to use .get(…) [Django-doc] directly. In case the object does not exists,
for example because the user altered the URL themselves, the get_object_or_404(…) will result in returning a HTTP 404 Not Found response, whereas using
.get(…) will result in a HTTP 500 Server Error.
Note: Normally one does not add a suffix _id to a ForeignKey field, since Django
will automatically add a "twin" field with an _id suffix. Therefore it should
be org, instead of org_id.
in your template change it to:
<h3>Hosting Body</h3>
{% for org in orgs%}
{%if org.org_id == event.org_id.org_id %}
<p>{{org.name}}</p>
{%endif%}
{%endfor%}
<h3>Date</h3>
<p>{{event.date}}</p>

I want to display a list of categories on my HTML page from a Django 2.2 model but having trouble

I am creating an e-commerce website with Django, and on the products page I have a sidebar with category selections. I would liek to dynamically display the category selections according to the category choices given in the product model.
Here is an image of the HTML (please don't judge the content or store type;-):
Here is the model for toy products:
class ToyProduct(models.Model):
slug = models.SlugField(max_length=40)
image = models.ImageField(upload_to='media')
title = models.CharField(max_length=150)
description = models.TextField()
price = models.DecimalField(max_digits=5, decimal_places=2)
stock_quantity = models.PositiveIntegerField()
in_stock = models.BooleanField(default=True)
category = models.CharField(choices=TOY_CATEGORY_CHOICES,
max_length=2, default='FW')
brand = models.CharField(choices=TOY_BRAND_CHOICES, max_length=2,
default='NB')
on_sale = models.BooleanField(default=False)
def __str__(self):
return self.title
Here are the category choices, defined within a tuple on the models.py file:
TOY_CATEGORY_CHOICES = (
('FW', 'For Women'),
('FM', 'For Men'),
('DD', 'Dildo'),
('VB', 'Vibrator'),
('LX', 'Luxury'),
('KT', 'Kit'),
('BP', 'Butt Plug'),
('PM', 'Prostate Massager'),
('AB', 'Anal Bead')
)
Here is the view in views.py
def products_sextoys_view(request):
context = {
'header': 'Sex Toys',
'toy': ToyProduct.objects.all()
}
return render(request, 'products/sextoys.html', context)
Here is the HTML file thus far:
<section id="products-framework">
<div class="container mt-5 mb-5">
<div class="row">
<div class="col-2" id="category-column">
<h1 class="text-center">Category Choices</h1>
<h4><strong>Category</strong></h4>
<h3></h3>
<hr class="newhr">
<h4><strong>Brands</strong></h4>
<h3></h3>
<hr class="newhr">
</div>
As you can see I have tried a for loop but this isn't outputting anything. I have tried several things, and various ways of typing out the for loop and referring to the choices object within the ToyProducts Class, however nothing seems to be working. There will be hundreds of pages with a sidebar and various choices within the models and so hard coding each and every page is not what I would like to do. I would like to be able to loop through the catrgory options and display them as a list down the left hand side
Many thanks!
EDIT: Now that we can see the view code. ToyProduct.objects.all() is a Queryset, which has no attribute category, so when you try to access toy.category you get nothing. Looping through toys to get categories will cause problems even if done correctly though. The best plan is definitely to just make sure the list of categories is available in the template context.
i can't be more specific or definitive because you haven't shown your code for the view, but: toy.category would most likely be a reference to the category assigned to whatever instance of ToyProduct is available in the template context as toy. to loop through all categories you'd want to make TOY_CATEGORY_CHOICES available to the template.
So I thought it would be possible to avoid having to create multiple category lists however apparently not, I created a dictionary categories and as the value created a list, containing each category option as a string. I then looped through this string within the HTML:
def products_sextoys_view(request):
context = {
'header': 'Sex Toys',
'toy': ToyProduct.objects.all(),
'categories': [
'For Women',
'For Men',
'Dildos',
'Vibrators',
'Luxury',
'Kits',
'Butt Plugs',
'Prostate Massagers',
'Anal Beads'
]
}
return render(request, 'products/sextoys.html', context)
And here is the HTML:
<div class="col-2" id="category-column">
<h1 class="text-center">Category Choices</h1>
<h4><strong>Category</strong></h4>
{% for category in categories %}
<h3>{{ category }}</h3>
{% endfor %}
<hr class="newhr">
<h4><strong>Brands</strong></h4>
<h3></h3>
<hr class="newhr">
</div>
I will be repeating the process for brand options too, thanks all!

Regroups a list of products by order with regroup template tag

I'm having problems using {% regroup %} Django template tag.
A brief summary: I succeed listing all my orders with their products ordered in the same template. So all seems to works fine doing the following:
Create the order
Create products ordered
Assing those products to the order
Display the daily orders with their products in the same template (filtered by date also) in dailyorders.html
Here my codes and only I'll show the code which allows me to display the orders and modify one if I want (where I have the problem)
models:
class Productsordered (models.Model):
item = models.ForeignKey(Itemlist, on_delete=models.CASCADE)
order = models.ForeignKey(Orders, on_delete=models.CASCADE)
quantity = models.IntegerField(default=1)
def __str__(self):
return f"{self.quant} - {self.item.nombre_producto}"
class Orders(models.Model):
date = models.DateField(default=date.today)
client_name = models.CharField(max_length=30) (just name, not client id)
def __str__(self):
return self.client_name
class Itemlist(models.Model):
id = models.SlugField(primary_key = True, max_length =30)
name_item = models.CharField(max_length=30)
price = models.IntegerField()
Just daily orders and modify order views:
def show_daily_orders(request,**kwargs):
daily_products_ordered =
Productsordered.objects.filter(order__date__day=date.today().day,
order__date__month=date.today().month,
order__date__year=date.today().year)
return render(request,'dailyorders.html',{'daily_products_ordered':daily_products_ordered})
def add_products_ordered (request,pk_order,pk_item):
products_ordered=Productsordered.objects.filter(order=pk_order)
get_order=Orders.objects.get(id=pk_pedido)
list= Itemlist.objects.all()
#ask if the product is already there, if not create one
try:
product = products_ordered.get(item=pk_item)
product.quantity += 1
product.save()
except:
newproduct = Itemlist.objects.get(id=pk_item)
newproduct = Productsordered.objects.create(item=newproduct, order=get_order)
product.save()
return render(request,'modify.html',{'list':list,'products_ordered':products_ordered,'order':order})
dailyorders.html would be something like:
{% regroup daily_products_ordered by order as order_list %}
{% for order in order_list %}
<h2> {{order.grouper}}
{% for product in order.list %}
<li> {{product.quantity}} - {{ product.item.name_item }}</li>
<a class = 'btn' href="{% url 'go_to_modify_order' pk=order.grouper.id %}">Modify order</a>
{% endfor %}
{% endfor %}
modify.html
{% for prod in list %}
<a class="btn" href="{% url 'go_to_modify_order' pk_item=prod.id pk_order=order.id %}">{{prod.name_item}}</a>
{% endfor %}
<a class="btn" href="{% url 'go_to_orders' pk_item=prod.id pk_pedido=order.id %}"> Finish modifying order </a>
# Also there would be the choice to delete an added product, but no matter for the problem.
The problem is that when I try to modify an order and add a new product (simply clicking the buttons and adding them to the cart) when I return to the template where I get all orders, regroup counts this new product as if it were from another order. For example:
Laura (order_id=1):
Product A
Product B
Carlos (order_id=2):
Product X
Laura (order_id=1):
Product C (this would be the product added when modify order!)
In order to see what happened, I went to the admin section and noticed that this new product is even added to the order I modified (Product C belonging to order_id=1 for example). So, it seems to be a problem when passing from the modify template to the daily orders, or something else I haven't noticed yet.
I'd appreciate help in this topic.
Thank you all
I've already done!
Seems to be that regroup only works when you have your dictionary order by the attribute you want to regroup! That's because I was getting the orders separately (see the example Laura (order_id=1) twice! see django documentation about regroup built-in template

Django: retrieving ManyToManyField objects with minimum set of queries

My code looks like this:
models.py
class Tag(models.Model):
name = models.CharField(max_length=42)
class Post(models.Model):
user = models.ForeignKey(User, related_name='post')
#...various fields...
tags = models.ManyToManyField(Tag, null=True)
views.py
posts = Post.objects.all().values('id', 'user', 'title')
tags_dict = {}
for post in posts: # Iteration? Why?
p = Post.objects.get(pk=[post['id']]) # one extra query? Why?
tags_dict[post['id']] = p.tags.all()
How am I supposed to create a dictionary with tags for each Post object with minimum set of queries? Is it possible to avoid iterating, too?
Yes you will need a loop. But you can save one extra query in each iteration, you don't need to get post object to get all its tags. You can directly query on Tag model to get tags related to post id:
for post in posts:
tags_dict[post['id']] = Tag.objects.filter(post__id=post['id'])
Or use Dict Comprehension for efficiency:
tags_dict = {post['id']: Tag.objects.filter(post__id=post['id']) for post in posts}
If you have Django version >= 1.4 and don't really need a dictionary, but need to cut down the count of queries, you can use this method like this:
posts = Post.objects.all().only('id', 'user', 'title').prefetch_related('tags')
It seems to execute only 2 queries (one for Post and another for Tag with INNER JOIN).
And then you can access post.tags.all without extra queries, because tags was already prefetched.
{% for post in posts %}
{% for tag in post.tags.all %}
{{ tag.name }}
{% endfor %}
{% endfor %}

Django: using aggregates to show vote counts for basic polls app

I'm making a very basic poll app. It's similar to the one in the Django tutorial but I chose to break out the vote counting aspect into its own model (the tutorial just adds a vote count field alongside each answer). Here's my models:
class PollQuestion(models.Model):
question = models.CharField(max_length=75)
class PollAnswer(models.Model):
poll = models.ForeignKey('PollQuestion')
answer = models.CharField(max_length=75)
class PollVote(models.Model):
poll = models.ForeignKey('PollQuestion')
answer = models.ForeignKey('PollAnswer')
date_voted = models.DateTimeField(auto_now_add=True)
user_ip = models.CharField(max_length=75)
I'm trying to show all of the vote counts for a given poll. Here's my view code:
from django.db.models import Count
poll_votes = PollVote.objects.select_related('PollAnswer').filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
When I output the results of this query I just get a single row per vote (eg I see about 40 'answers' for my poll, each one representing a vote for one of the 5 actual PollAnswers). If I look at the queries Django makes, it runs something like this for every vote in the poll:
SELECT `poll_answers`.`id`, `poll_answers`.`poll_id`, `poll_answers`.`answer`
FROM `poll_answers`
WHERE `poll_answers`.`id` = 101
Can anyone poke me in the right direction here? I get the feeling this should be easy.
EDIT: here's my template code, for completeness.
<ul>
{% for vote in votes %}
{{ vote.answer }} ({{ votes.num_votes }})<br />
{% endfor %}
</ul>
Try:
poll_votes = PollVote.objects.filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
or:
poll_votes = PollVote.objects.values('poll', 'answer__answer').filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
Relevant docs:
Django offical docs
Never mind, fixed it myself after finding a tutorial which uses the same sort of models as me.
Essentially the fix was in the view:
p = get_object_or_404(PollQuestion, pk=poll_id)
choices = p.pollanswer_set.all()
And in the template:
{% for choice in choices %}
<p class="resultsList">{{choice.answer}} - {{choice.pollvote_set.count}}</p>
{% endfor %}