Django prefetch_related() in ListView class based view - django

I have a simple blog with 2 models: one for Post and one for Comment, like so:
class Post(models.Model):
title = models.CharField(max_length=100)
# content = models.TextField()
content = RichTextUploadingField(blank=True, null=True, config_name='claudiu_cfg')
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
class Comment(models.Model):
author = models.CharField(max_length=20)
text = models.TextField(max_length=350)
date_posted = models.DateTimeField(default=timezone.now)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
def __str__(self):
return self.author
I want to display all posts (paginated) and how many comments each have.
My views.py:
class PostListView(ListView):
model = Post
template_name = 'blog/home.html'
context_object_name = 'posts'
ordering = ['-date_posted']
paginate_by = 5
Just like this i was getting all posts, but now i want to access the Comment table with as little selects as possible.
I know one way to do this is define a queryset and did it like this:
class PostListView(ListView):
model = Post
template_name = 'blog/home.html' # <app>/<model>_<viewtype>.html
context_object_name = 'posts'
ordering = ['-date_posted']
paginate_by = 5
queryset = Post.objects.all().prefetch_related()
However that does not access the Comment data.
Then i tried to overrite the get_queryset() function hopping to get the desired result. Still no success.
def get_queryset(self):
posts = Post.objects.all().prefetch_related('pk__comment').all()
comments = Comment.objects.all().prefetch_related('post')
print(dir(posts))
print('---------------------')
print(posts._prefetch_done)
return posts.order_by('-date_posted')
That still doesnt work and i cant understand where to go from here.
I did read the documentation, but that still didnt help me.
Please help, as i have nobody to ask for help when it comes to django.
It looks like the solution was closer than i expected, based on responses from
#dirkgroten i managed to fix my issue like so:
class PostListView(ListView):
model = Post
template_name = 'blog/home.html' # <app>/<model>_<viewtype>.html
context_object_name = 'posts'
ordering = ['-date_posted']
paginate_by = 5
queryset = Post.objects.all().prefetch_related('comment_set').all()
To display the number of comments in my template, in a for loop i added:
{{ post.comment_set.all|length }}
Thank you, without your help it would have taken me atleast a few more hours if not days.

Related

How to filter data dynamically by supply values from form using django

I want to filter Blog Post objects or records based on the Post Category and a User that uploaded the Post record, it gives me an error when I try to do filter, this is the error.
ValueError at /dashboard/filter-post/
The QuerySet value for an exact lookup must be limited to one result using slicing.
Here is my models.py
class Category(models.Model):
cat_name = models.CharField(max_length=100, verbose_name='Category Name')
cat_desc = models.TextField(blank=True, null=True)
def __str__(self):
return self.cat_name
class Meta():
verbose_name_plural='Category'
class Post(models.Model):
pst_title = models.CharField(max_length=150)
pst_image = models.ImageField(blank=True, null=True, upload_to='uploads/')
user = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ManyToManyField(Category)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.pst_title
#property
def img_url(self):
if self.pst_image:
return self.pst_image.url
on forms.py
class FilterForm(forms.ModelForm):
user = forms.ModelChoiceField(
queryset=User.objects.all(),
widget=forms.Select(attrs={'class': 'form-control'}))
category = forms.ModelMultipleChoiceField(
queryset=Category.objects.all(),
widget=forms.SelectMultiple(attrs={'class': 'form-control js-example-disabled-results'}))
catch_bot = forms.CharField(required=False,
widget=forms.HiddenInput, validators=[validators.MaxLengthValidator(0)])
class Meta():
fields = ['user', 'category' ]
model = Post
on views.py
def filter_post(request):
post = FilterForm(request.GET)
queryset = Post.objects.all()
if post.is_valid():
user=post.cleaned_data.get('user')
category=post.cleaned_data.get('category')
if user and category:
queryset = queryset.filter(user__username=user, category__cat_name=category)
return render(request, 'backend/filter-post.html', {'query':queryset, 'post':post})
I am having challenges properly filtering this in my views any help?
Try this:
instead of this:
queryset = queryset.filter(user__username=user, category__cat_name=category)
use this:
queryset = queryset.filter(user=user, category=category)
Also don't name your model fields after the model name, just use name instead of pst_name or cat_name, you will see that when you will try access these values there will be no confusion.
UPDATE
Ok, maybe try to rewrite your view like this:
def filter_post(request):
posts = Post.objects.all()
form = FilterForm(request.GET) # its best practice to call your form instance `form` in the view so that the next line has better readability
if form.is_valid():
user=post.cleaned_data['user']
category=post.cleaned_data['category']
if user:
posts = posts.filter(user=user)
if category:
posts = posts.filter(category=category)
return render(request, 'backend/filter-post.html', {'posts':posts})

Detail views fail to find post url after adding one to many fk relationship from "PostFile" to "Post"

After changing admin.py (previous code commented):
# admin.site.register(Post)
class PostFileAdmin(admin.StackedInline):
model = PostFile
#admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [PostFileAdmin]
#admin.register(PostFile)
class PostFileAdmin(admin.ModelAdmin):
pass
And adding PostFile to models.py:
class Post(models.Model):
slug = models.SlugField(max_length=200, unique_for_date="date_posted")
content = RichTextField()
date_posted = models.DateTimeField(default=timezone.now)
...
def get_absolute_url(self):
return reverse("news:news-detail",
args=[self.date_posted.year,
self.date_posted.month,
self.date_posted.day,
self.slug])
class PostFile(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
post_file = models.FileField(upload_to="files/news", null=True, blank=True)
Also, views.py:
class PostListView(ListView):
model = Post
template_name = "news/news_home.html"
context_object_name = "posts"
ordering = ["-pk"]
paginate_by = 2
def news_detail(request, year, month, day, post):
post = get_object_or_404(Post, slug=post,
status="published",
date_posted__year=year,
date_posted__month=month,
date_posted__day=day)
return render(request, "news/news_detail.html", {"post": post,})
When I create new post in Django admin, and then try to go to its absolute_url to see detailed view, it raises 404. I checked, using shell, absolute_url is good and all objects are created properly, but for some reason post's URL is broken. Any idea how to solve this?

Getting pk from selected object in a generic ListView html to populate a FK for a new form (CBV)

I'm trying to make a carwash app and I am having issues getting the wash form populated with the vehicle's pk. I've tried with "get_initial", "get_context_data", "form_valid", passing {% url 'appname:urlname' object.pk %} in a html button, even thought about formsets. So if you can guide this noob in polishment (me), I would greeeeaaaatly apreciate it!
so this is what the code is simplified to...
model:
class Car(models.Model):
carplates = models.CharField(max_length=50, unique=True, null=False)
owner = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
class Wash(models.Model):
vehicle_to_wash = models.ForeignKey(Car, on_delete=models.CASCADE)
specific_comment = models.TextField(max_length=500, blank=True, null=True
form:
class WashServiceForm(forms.ModelForm):
class Meta:
model = Wash
fields = ['specific_comment', 'vehicle_to_wash']
views:
class CarList(LoginRequiredMixin, ListView):
model = Car
def get_queryset(self):
return Car.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super(CarList, self).get_context_data(**kwargs)
context['car_list'] = context['object_list'].filter(user=self.request.user)
return context
class WashService(LoginRequiredMixin, CreateView):
model = Wash
form_class = WashServiceForm
template_name = 'service_app/standard_wash_form.html'
success_url = reverse_lazy('service_app:wash_review')
class WashReview(LoginRequiredMixin, TemplateView):
model = Wash
urls:
app_name = 'service_app'
urlpatterns = [
path('car-list/', CarList.as_view(), name='car_list'),
path('<int:id>/select_wash/', WashService.as_view(), name='wash_service'),
path('<int:pk>/review', WashReview.as_view(), name='wash_review'),
]
For class based views you can use self.kwargs.get('parameter')
You also need to add get_success_url to your class WashService
class WashService(LoginRequiredMixin, CreateView):
model = Wash
form_class = WashServiceForm
template_name = 'service_app/standard_wash_form.html'
def get_success_url(self):
return reverse('service_app:wash_review',args=[self.object.id])

How to update two models in one form in django?

Okay, so first of all the situation is not quite easy as in the title. So I want to create form which can create object based on Cycle model and update only one field in Post model (field in question is 'cycle_title'). What is more its isn't only one post where this post have to be updated but there are several of it (all post's titles are saved in Cycle.post).
views
class CycleCreateView(LoginRequiredMixin, BSModalCreateView):
template_name = 'blog/cycle_form.html'
form_class = CycleForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update(user=self.request.user)
return kwargs
def form_valid(self, form, **kwargs):
form.instance.author = self.request.user
return super().form_valid(form)
def get_success_url(self):
reverse_user = self.request.user
return reverse('profile', kwargs={'username': reverse_user})
forms
class CycleForm(BSModalForm):
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
if user is not None:
self.fields['posts'].queryset = Post.objects.filter(author=user)
class Meta:
model = Cycle
fields = ['title', 'description', 'posts']
widgets = {
'posts': forms.CheckboxSelectMultiple(),
}
models
class Post(models.Model):
title = models.CharField(max_length=100, unique=True)
content = MDTextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
cycle_title = models.CharField(max_length=100, default='')
class Cycle(models.Model):
title = models.CharField(max_length=100, unique=True)
description = models.TextField(max_length=500, default="Brak opisu")
date_created = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
posts = models.ManyToManyField(Post)
I was thinking about a solution like this:
for i in form.cleaned_data['posts']:
post = Post.objects.get(title=form.cleaned_data['title'][i])
post.cycle_title = form.cleaned_data['title']
post.save()
But I doubt if it is good way to resolve this issue
A package has been built just to handle this exact scenario, django-shapeshifter. You can find it here:
https://github.com/kennethlove/django-shapeshifter
The basic premise is to create two model forms, then include them in the same template. The example given shows how to update a User and a Profile model from the same view and form. It sounds like that is a match for your problem. Full disclosure, I'm a contributor to this package, but is was created exactly because of frustrations like your own!

Using prefetch_related for reducing queries overhead

I have two models.
class Category(models.Model):
title = models.CharField(max_length=60, unique=True)
def __unicode__(self):
return self.title
class Post(models.Model):
title = models.CharField(max_length=60, unique=True)
category = models.ForeignKey(Category)
#code
views.py
class PostList(ListView):
model = Post
def get_queryset(self):
queryset = Post.objects.all().\
select_related('category')
But for my main page i need further all categories for the navbar. What the best approach to add categories to get_queryset()?
I tried to use prefetch_related.
post = Post.objects.all().prefetch_related('category')
but i don't understand how to fetch all categories.
Is it correct solution?
class PostList(ListView):
def get_queryset(self):
p = Post.objects.all().\
select_related('category') #like tag for each post
p.categories = Category.objects.all() #all categories for navbar
return p
Or Django has own methods for this task?
I don't understand what this has to do with either select_related or prefetch_related. Neither of those will help in getting categories that are not related to your posts: as their names imply, they are to do with getting related items, not unrelated ones.
If you need all the categories, you should simply do Category.objects.all().