I'm building a news app that allows members to post comments on articles. I want to display the articles in my template and also display the number of comments that have been made on each article. I tried using the count method but, it retrieved the total number of comments in my comments table instead of the number of comments that a particular article has.
#models.py
class Article(models.Model):
#auto-generate indices for our options
ENTRY_STATUS = enumerate(('no', 'yes'))
#this will be a foreign key once account app is built
author = models.CharField(default=1, max_length=1)
category = models.ForeignKey(Category)
title = models.CharField(max_length=50)
entry = models.TextField()
dateposted = models.DateTimeField(default=timezone.now, auto_now_add=True)
draft = models.IntegerField(choices=ENTRY_STATUS, default=0)
lastupdated = models.DateTimeField(default=timezone.now, auto_now=True)
#prevents the generic labeling of our class as 'Classname object'
def __unicode__(self):
return self.title
class Comment(models.Model):
#this will be a foreign key once account app is built
author = models.CharField(default=1, max_length=1)
article = models.ForeignKey(Article)
dateposted = models.DateTimeField(auto_now_add=True)
comment = models.TextField()
def __unicode__(self):
#returns the dateposted as a unicode string
return unicode(self.dateposted)
#templates/list_articles.html
{% for comment in comments %}
{% if comment.article_id == article.id %}
{% if comments.count < 2 %}
#this is returning all comments in comment table
<b>{{ comments.count }} comment</b>
{% else %}
<b>{{ comments.count }} comments</b>
{% endif %}
{% endif %}
{% endfor %}
All the examples I've seen so far manually provide a value to filter by(e.g. Comment.objects.filter(article_id=x).count() ) In my case I only have access via the template.
#views.py
class ArticlesListView(ListView):
context_object_name = 'articles'
# only display published pieces (limit 5)
queryset = Article.objects.select_related().order_by('-dateposted').filter(draft=0)[:5]
template_name = 'news/list_articles.html'
# overide this to pass additional context to templates
def get_context_data(self, **kwargs):
context = super(ArticlesListView, self).get_context_data(**kwargs)
#get all comments
context['comments'] = Comment.objects.order_by('-dateposted')
#get all article photos
context['photos'] = Photo.objects.all()
#context['total_comments'] = Comment.objects.filter(article_id=Article)
return context
My intended result is to have a listing of all articles and a roll-up of comments made on that article below each article(e.g. Article 1: 4 comments, Article 5: 1 comment, etc...) Right now I'm getting: Article 1: 4 comments, Article 5: 4 comments(even though Article 5 only has 1 comment)
Any help is appreciated. I've spent 5 hours reading through the documentation but every example manually provides a value to filter by.
I'm not sure why you find this unexpected. comments is all the comments, so of course comments.count is a count of all the comments. How could it be otherwise? You don't filter them anywhere.
This is however a really really horrible way to do things. There is absolutely no reason to pass all comments to the template and then iterate through them to check if they're the right article. You have a foreign key from Comment to Article, so you should use the reverse relationship to get the relevant commments.
Leave out the Comment query altogether from your view, and in your template just do this (replacing that whole block of nested fors and ifs):
{{ article.comment_set.count }}
This however does one count query per article. A better solution is to use annotations, so you can do it all in one single query. Change your queryset to add the annotated count of related comments:
from django.db.models import Count
class ArticlesListView(ListView):
queryset = Article.objects.select_related().annotate(comment_count=Count('comments')).order_by('-dateposted').filter(draft=0)
and now you can just do
{{ article.comment_count }}
Related
Hello friends I am trying to figure out how to work properly with a One to Many relationship.
I want to create two models,
The first model is Post
And a second model is Comment
Now I say that every post has many comments, and every comment has one post.
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
message = models.TextField(max_length=256,null=True)
def __str__(self):
return str(self.title)
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
message = models.TextField(max_length=256, null=True)
I'm trying to figure out how I can get all the posts and all the comments so that all the comments match the post to which they belong
That is, in the functions in the views file how do I send back this option.
because on the HTML page I want to display the post with all its comments
but there are a number of posts, so how do I do that?
The only thought I can do is this:
dict = {}
all_posts = Post.objects.all()
for post in all_posts:
dict[post] = Comment.objects.filter(post=post).values()
print(dict)
but I have a feeling there is something better
You can access all related comments for a post by using post.comment_set.all() see Following relationships “backward”
{% for post in all_posts %}
{{ post }}
{% for comment in post.comment_set.all %}
{{ comment }}
{% endfor %}
{% endfor %}
To reduce the number of queries use prefetch_related to get all related comments for all posts in a single query
def posts_list(request):
all_posts = Post.objects.prefetch_related('comment_set')
return render(request, 'template.html', {'all_posts': all_posts})
I try to build a blog and this blog in the home view www.site.com consist of posts and these posts have comments, Now I Show the posts using List [] because the user has the ability to follow the content and in this list, I show the content based on the user, Now I successfully to show the posts but this post contains comments that's mean I need to get the pk of the post but as I said this post in the home view www.site.com without any extra URL that's mean as My knowledge I can't pass the pk in the def home_screen_view(request, pk) because this raise error home_screen_view() missing 1 required keyword-only argument: 'pk'
So my qustion how can I get the pk in the base url www.site.com
My view
def home_screen_view(request, *args, **kwargs):
users = [user for user in profile.following.all()]
post = []
for u in users:
p = Account.objects.get(username=u)
posts = p.post_set.all()
post.append(posts)
my_posts = request.user.post_set.all()
post.append(my_posts)
if len(post):
post= sorted(chain(*post), reverse=True, key=lambda post: post.created_date)
posts = Post.objects.filter(pk=post.pk) # here I want to get the pk of the post in order to show the comments related this post
comment = PostCommentIDE.objects.filter(post=posts)
The url
path('', home_screen_view, name='home'),
My Post Model
class Post(models.Model):
author = models.ForeignKey(Account, on_delete=models.CASCADE)
article = models.TextField(null=True, blank=True)
photo_article = models.ImageField(max_length=255, upload_to=get_poster_filepath)
created_date = models.DateTimeField(auto_now_add=True)
My Comment Model
class PostCommentIDE(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='ide_com')
author = models.ForeignKey(Account, on_delete=models.CASCADE)
content = models.TextField()
created_date = models.DateTimeField(auto_now_add=True)
The post template
{% for post in posts %}
...
#here I want to render the comments that related to spesific post
{% for comment in comments %}
{{ comments.content }}
{% endfor %}
...
{% endfor %}
I use function based view
From your home_screen_view you can remove
comment = PostCommentIDE.objects.filter(post=posts)
Instead, in your template you can do:
{% for comment in post.ide_com.all %}
{{ comments.content }}
{% endfor %}
Explanation:
Your comment model PostCommentIDE has a ForeignKey relationship with Post. This enables you to get the related comments for a post. By default you could have accessed the comments with post.postcommentide_set.all, but as you've defined a related_name attribute on that relationship, it becomes post.ide_com.all. Read more about it here: https://docs.djangoproject.com/en/3.2/ref/templates/language/#accessing-method-calls
I'm struggling getting the right query for my project. Here is an example or my model :
from django.db import models
class Pictures(models.Model):
name = models.CharField(max_length=100)
bild = models.FileField(upload_to='article_pictures/')
articel = models.ForeignKey('articles', on_delete=models.CASCADE)
def __str__(self):
return self.name
class Articles(models.Model):
name = models.CharField(max_length=100)
text = models.TextField(max_length=2000)
published = models.BooleanField(default=False)
def __str__(self):
return self.name
how do I get the published artikles from the artikles class including the pictures (if there is one, or more)?
Thank you for your help
I don't think there is any exact query for this, but you can use prefetch_related to pre-load data from database. For example:
articles = Artikles.objects.filter(published=True).prefetch_related('pictures_set')
for article in articles:
article.pictures_set.all() # will not hit database
All published articles:
Articles.objects.filter(published=True)
A single published Article(Example):
article = Articles.objects.filter(published=True).first()
# and it's pictures
for picture in article.pictures_set.all():
print(picture)
Note: models have singular names, so you should rename Articles to Article and Pictures to Picture.
The related Pictures of an article article can be obtained with:
my_article.picture_set.all()
this is a queryset that contains all the related pictures.
We can obtain the Articles that are publised, and then fetch the related Pictures in two extra queries with:
articles = Article.objects.filter(published=True).prefetch_related('picture_set')
So in a template you can then render it like:
{% for article in articles %}
{{ article.name }}
{% for picture in article.picture_set.all %}
{{ picture.name }}
{% endfor %}
{% endfor %}
I have List of articles to be displayed as per date. Every User can mark any article as his favourite.
If a user is logged in, then he should be able to see some indication whether the article is already marked as his favourite or not.
the following are my models:
class Favourite(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
article = models.ForeignKey(Article, null=True,on_delete=models.CASCADE)
class Meta:
unique_together = ('user', 'article')
class Article(models.Model):
title = models.CharField(max_length=256)
intro_image = models.ImageField(null=True, blank=True, upload_to=doc_hash)
description = models.TextField(null=True, blank=True)
I was thinking for every article get all the user ids who marked that article as favourite and then later check it at front end with the current user.
But if some article is marked as favourite by 1000 users , then unneccessarily i will have to get the data of 1000 users along with that article which is too much.
Is there a way if i pass the user i can get the favourite data only with respect to that user for each article so that i can save both on queries and amount of data to be passed to front end.
Either do that in your view's context or as a custom template tag.
The examples below are dry-coded, so they might have silly bugs.
In a view (assuming a class-based view):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['is_favourite'] = Favourite.objects.filter(user=self.request.user, article=self.object).exists()
return context
Usage:
{% if is_favourite %}Yay, you like it!{% endif %}
or a template tag:
#register.simple_tag(takes_context=True)
def is_favourite(context, article):
request = context['request']
return Favourite.objects.filter(user=request.user, article=article).exists()
Usage:
{% is_favourite article as fav %}
{% if fav %}Yay, you like it!{% endif %}
Edit
For a ListView, you can do something like
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['favourited_ids'] = set(Favourite.objects.filter(user=self.request.user, article__in=context['object_list']).values_list('article_id', flat=True))
return context
and use it
{% if article.id in favourited_ids %}Yay, you like this article!{% endif %}
I assume that you need a flag to state if the user has already marked the article as favourite or not, suppose there are 100 articles and out of that a user has marked 40 articles as favourite then when the data would be sent then 100 articles data is sent with 40 articles having flag as read TRUE and rest as FALSE.
Following is the SQL equivalent, which you can convert to Django ORM as per your need. 'xyz' is the user id for which you need to show the articles
SELECT article.*,
CASE
WHEN read.article_id IS NOT NULL then TRUE
ELSE FALSE
as read_flag
from Article as article
left join
(select article_id from Favourite where user_id = 'xyz') as read
on article.id = read.article_id
I found the below answer from: https://stackoverflow.com/a/51889455/2897115. Using annotations and subquery. Django can be faster using annotations and subquery which i read from. https://medium.com/#hansonkd/the-dramatic-benefits-of-django-subqueries-and-annotations-4195e0dafb16
I am putting the solution given in https://stackoverflow.com/a/51889455/2897115. Using annotations
qs = Post.objects.all()
sub = Like.objects.filter(post__id=OuterRef('pk'), user=request.user)
values = qs.annotate(
date=Func(F('date'), function='db_specific_date_formatting_func'),
current_user_like=Exists(sub)
).values('text, 'date', 'current_user_like')
I have three different models for my app. All are working as I expected.
class Tender(models.Model):
title = models.CharField(max_length=256)
description = models.TextField()
department = models.CharField(max_length=50)
address = models.CharField(max_length=50)
nature_of_work = models.CharField(choices=WORK_NATURE, max_length=1)
period_of_completion = models.DateField()
pubdat = models.DateTimeField(default=timezone.now)
class Job(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
title = models.CharField(max_length=256)
qualification = models.CharField(max_length=256)
interview_type = models.CharField(max_length=2, choices=INTERVIEW_TYPE)
type_of_job = models.CharField(max_length=1, choices=JOB_TYPE)
number_of_vacancies = models.IntegerField()
employer = models.CharField(max_length=50)
salary = models.IntegerField()
pubdat = models.DateTimeField(default=timezone.now)
class News(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
title = models.CharField(max_length=150)
body = models.TextField()
pubdat = models.DateTimeField(default=timezone.now)
Now I am displaying each of them at separate page for each of the model (e.g. in the jobs page, I am displaying only the jobs.). But now at the home page, I want to display these according to their published date at the same page. How can I display different objects from different models at the same page? Do I make a separate model e.g. class Post and then use signal to create a new post whenever a new object is created from Tender, or Job, or News? I really hope there is a better way to achieve this. Or do I use multi-table inheritance? Please help me. Thank you.
Update:
I don't want to show each of the model objects separately at the same page. But like feeds of facebook or any other social media. Suppose in fb, any post (be it an image, status, share) are all displayed together within the home page. Likewise in my case, suppose a new Job object was created, and after that a new News object is created. Then, I want to show the News object first, and then the Job object, and so on.
A working solution
There are two working solutions two other answers. Both those involve three queries. And you are querying the entire table with .all(). The results of these queries combined together into a single list. If each of your tables has about 10k records, this is going to put enormous strain on both your wsgi server and your database. Even if each table has only 100 records each, you are needlessly looping 300 times in your view. In short slow response.
An efficient working solution.
Multi table inheritance is definitely the right way to go if you want a solution that is efficient. Your models might look like this:
class Post(models.Model):
title = models.CharField(max_length=256)
description = models.TextField()
pubdat = models.DateTimeField(default=timezone.now, db_index = True)
class Tender(Post):
department = models.CharField(max_length=50)
address = models.CharField(max_length=50)
nature_of_work = models.CharField(choices=WORK_NATURE, max_length=1)
period_of_completion = models.DateField()
class Job(Post):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
qualification = models.CharField(max_length=256)
interview_type = models.CharField(max_length=2, choices=INTERVIEW_TYPE)
type_of_job = models.CharField(max_length=1, choices=JOB_TYPE)
number_of_vacancies = models.IntegerField()
employer = models.CharField(max_length=50)
salary = models.IntegerField()
class News(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
def _get_body(self):
return self.description
body = property(_get_body)
now your query is simply
Post.objects.select_related(
'job','tender','news').all().order_by('-pubdat') # you really should slice
The pubdat field is now indexed (refer the new Post model I posted). That makes the query really fast. There is no iteration through all the records in python.
How do you find out which is which in the template? With something like this.
{% if post.tender %}
{% else %}
{% if post.news %}
{% else %}
{% else %}
Further Optimization
There is some room in your design to normalize the database. For example it's likely that the same company may post multiple jobs or tenders. As such a company model might come in usefull.
An even more efficient solution.
How about one without multi table inheritance or multiple database queries? How about a solution where you could even eliminate the overhead of rendering each individual item?
That comes with the courtesy of redis sorted sets. Each time you save a Post, Job or News, object you add it to a redis sorted set.
from django.db.models.signals import pre_delete, post_save
from django.forms.models import model_to_dict
#receiver(post_save, sender=News)
#receiver(post_save, sender=Post)
#receiver(post_save, sender=Job)
def add_to_redis(sender, instance, **kwargs):
rdb = redis.Redis()
#instead of adding the instance, you can consider adding the
#rendered HTML, that ought to save you a few more CPU cycles.
rdb.zadd(key, instance.pubdat, model_to_dict(instance)
if (rdb.zcard > 100) : # choose a suitable number
rdb.zremrangebyrank(key, 0, 100)
Similarly, you need to add a pre_delete to remove them from redis
The clear advantage of this method is that you don't need any database queries at all and your models continue to be really simple + you get catching thrown in the mix. If you are on twitter your timeline is probably generated through a mechanism similar to this.
The following should do want you need. But to improve performance you can create an extra type field in each of your models so the annotation can be avoided.
Your view will look something like:
from django.db.models import CharField
def home(request):
# annotate a type for each model to be used in the template
tenders = Tender.object.all().annotate(type=Value('tender', CharField()))
jobs = Job.object.all().annotate(type=Value('job', CharField()))
news = News.object.all().annotate(type=Value('news', CharField()))
all_items = list(tenders) + list(jobs) + list(news)
# all items sorted by publication date. Most recent first
all_items_feed = sorted(all_items, key=lambda obj: obj.pubdat)
return render(request, 'home.html', {'all_items_feed': all_items_feed})
In your template, items come in the order they were sorted (by recency), and you can apply the appropriate html and styling for each item by distinguishing with the item type:
# home.html
{% for item in all_items_feed %}
{% if item.type == 'tender' %}
{% comment "html block for tender items" %}{% endcomment %}
{% elif item.type == 'news' %}
{% comment "html block for news items" %}{% endcomment %}
{% else %}
{% comment "html block for job items" %}{% endcomment %}
{% endif %}
{% endfor %}
You may avoid the annotation altogether by using the __class__ attribute of the model objects to distinguish and put them in the appropriate html block.
For a Tender object, item.__class__ will be app_name.models.Tender where app_name is the name of the Django application containing the model.
So without using annotations in your home view, your template will look:
{% for item in all_items_feed %}
{% if item.__class__ == 'app_name.models.Tender' %}
{% elif item.__class__ == 'app_name.models.News' %}
...
{% endif %}
{% endfor %}
With this, you save extra overhead on the annotations or having to modify your models.
A straight forward way is to use chain in combination with sorted:
View
# your_app/views.py
from django.shortcuts import render
from itertools import chain
from models import Tender, Job, News
def feed(request):
object_list = sorted(chain(
Tender.objects.all(),
Job.objects.all(),
News.objects.all()
), key=lambda obj: obj.pubdat)
return render(request, 'feed.html', {'feed': object_list})
Please note - the querysets mentioned above using .all() should be understood as placeholder. As with a lot of entries this could be a performance issue. The example code would evaluate the querysets first and then sort them. Up to some hundreds of records it likely will not have a (measurable) impact on performance - but in a situation with millions/billions of entries it is worth looking at.
To take a slice before sorting use something like:
Tender.objects.all()[:20]
or use a custom Manager for your models to off-load the logic.
class JobManager(models.Manager):
def featured(self):
return self.get_query_set().filter(featured=True)
Then you can use something like:
Job.objects.featured()
Template
If you need additional logic depending the object class, create a simple template tag:
#templatetags/ctype_tags.py
from django import template
register = template.Library()
#register.filter
def ctype(value):
return value.__class__.__name__.lower()
and
#templates/feed.html
{% load ctype_tags %}
<div>
{% for item in feed reversed %}
<p>{{ item|ctype }} - {{ item.title }} - {{ item.pubdat }}</p>
{% endfor %}
</div>
Bonus - combine objects with different field names
Sometimes it can be required to create these kind of feeds with existing/3rd party models. In that case you don't have the same fieldname for all models to sort by.
DATE_FIELD_MAPPING = {
Tender: 'pubdat',
Job: 'publish_date',
News: 'created',
}
def date_key_mapping(obj):
return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
def feed(request):
object_list = sorted(chain(
Tender.objects.all(),
Job.objects.all(),
News.objects.all()
), key=date_key_mapping)
Do I make a separate model e.g. class Post and then use signal to
create a new post whenever a new object is created from Tender, or
Job, or News? I really hope there is a better way to achieve this. Or
do I use multi-table inheritance?
I don't want to show each of the model objects separately at the same
page. But like feeds of facebook or any other social media.
I personally don't see anything wrong using another model, IMHO its even preferable to use another model, specially when there is an app for that.
Why? Because I would never want to rewrite code for something which can be achieved by extending my current code. You are over-engineering this problem, and if not now, you are gonna suffer later.
An alternative solution would be to use Django haystack:
http://haystacksearch.org/
http://django-haystack.readthedocs.io/en/v2.4.1/
It allows you to search through unrelated models. It's more work than the other solutions but it's efficient (1 fast query) and you'll be able to easily filter your listing too.
In your case, you will want to define pubdate in all the search indexes.
I cannot test it right now, but you should create a model like:
class Post(models.Model):
pubdat = models.DateTimeField(default=timezone.now)
tender = models.ForeignKey('Tender')
job = models.ForeignKey('Job')
news = models.ForeignKey('News')
Then, each time a new model is created, you create a Post as well and relate it to the Tender/Job/News. You should relate each post to only one of the three models.
Create a serializer for Post with indented serializers for Tender, Job and News.
Sorry for the short answer. If you think it can work for your problem, i'll write more later.