diff --git a/static/website/templates/ajax-keyword-search.html b/static/website/templates/ajax-keyword-search.html index b144529..68cb41d 100755 --- a/static/website/templates/ajax-keyword-search.html +++ b/static/website/templates/ajax-keyword-search.html @@ -63,16 +63,16 @@ - {{ question.views}} + {{ question.views }} - {{ question.answer_set.count }} + {{ question.total_answers }} - - {{ question.user|truncatechars:10 }} + + {{ question.cached_user|default:question.user|truncatechars:10 }} diff --git a/static/website/templates/ajax-time-search.html b/static/website/templates/ajax-time-search.html index 57be751..c26e4b7 100755 --- a/static/website/templates/ajax-time-search.html +++ b/static/website/templates/ajax-time-search.html @@ -61,16 +61,16 @@ - {{ question.views}} + {{ question.views }} - {{ question.answer_set.count }} + {{ question.total_answers }} - - {{ question.user|truncatechars:10 }} + + {{ question.cached_user|default:question.user|truncatechars:10 }} diff --git a/static/website/templates/filter.html b/static/website/templates/filter.html index 680fe28..f1b6e62 100755 --- a/static/website/templates/filter.html +++ b/static/website/templates/filter.html @@ -82,16 +82,16 @@
- {{ question.views}} + {{ question.views }} - + - {{ question.answer_set.count }} + {{ question.total_answers }} - + - - {{ question.user|truncatechars:10 }} + + {{ question.cached_user|default:question.user|truncatechars:10 }} diff --git a/static/website/templates/get-question.html b/static/website/templates/get-question.html index 7bc8bc0..1348ab5 100755 --- a/static/website/templates/get-question.html +++ b/static/website/templates/get-question.html @@ -136,7 +136,7 @@
Question
- {{ question.user }} + {{ question.cached_user|default:question.user }}
@@ -169,7 +169,7 @@

Answers:

- {{ answer.user }} + {{ answer.cached_user|default:answer.user }}
{% if user|can_edit:answer %} @@ -199,7 +199,7 @@

Answers:

- {{ comment.user }} + {{ comment.cached_user|default:comment.user }}
{% if user|can_edit:comment %} diff --git a/static/website/templates/notifications.html b/static/website/templates/notifications.html index aa3adce..68f833f 100755 --- a/static/website/templates/notifications.html +++ b/static/website/templates/notifications.html @@ -7,7 +7,7 @@

Notifications

{% for notification in notifications %} - {% get_notification notification.id %} + {% get_notification notification %} {% endfor %} {% endblock %} diff --git a/static/website/templates/questions.html b/static/website/templates/questions.html index cfb0c9e..5907aec 100755 --- a/static/website/templates/questions.html +++ b/static/website/templates/questions.html @@ -68,16 +68,16 @@
- {{ question.views}} + {{ question.views }} - {{ question.answer_set.count }} + {{ question.total_answers }} - - {{ question.user|truncatechars:10 }} + + {{ question.cached_user|default:question.user|truncatechars:10 }} diff --git a/static/website/templates/recent-questions.html b/static/website/templates/recent-questions.html index 3461908..ed80301 100755 --- a/static/website/templates/recent-questions.html +++ b/static/website/templates/recent-questions.html @@ -1,9 +1,7 @@ -{% extends 'website/templates/base.html' %} {% load static %} {% load count_tags %} -{% block content %} -

Recent Questions

+

Recent Questions

@@ -65,16 +63,16 @@

Recent Questions

@@ -96,11 +94,6 @@

Recent Questions

{% endfor %} {% endif %} - -{% endblock %} - -{% block javascript %} -{% endblock %} diff --git a/website/models.py b/website/models.py index cd9040e..b45e2e9 100755 --- a/website/models.py +++ b/website/models.py @@ -21,12 +21,22 @@ class Question(models.Model): # votes = models.IntegerField(default=0) def user(self): + cached_username = getattr(self, "cached_user", None) + if cached_username is not None: + return cached_username user = User.objects.get(id=self.uid) - return user.username + username = user.username + self.cached_user = username + return username def last_post_user(self): + cached_username = getattr(self, "cached_last_post_user", None) + if cached_username is not None: + return cached_username user = User.objects.filter(id=self.last_post_by).first() - return user.username if user else "Unknown User" + username = user.username if user else "Unknown User" + self.cached_last_post_user = username + return username class Meta: get_latest_by = "date_created" @@ -54,8 +64,13 @@ class Answer(models.Model): # votes = models.IntegerField(default=0) def user(self): + cached_username = getattr(self, "cached_user", None) + if cached_username is not None: + return cached_username user = User.objects.get(id=self.uid) - return user.username + username = user.username + self.cached_user = username + return username class AnswerVote(models.Model): @@ -71,8 +86,13 @@ class AnswerComment(models.Model): date_modified = models.DateTimeField(auto_now=True) def user(self): + cached_username = getattr(self, "cached_user", None) + if cached_username is not None: + return cached_username user = User.objects.get(id=self.uid) - return user.username + username = user.username + self.cached_user = username + return username class Notification(models.Model): @@ -84,7 +104,12 @@ class Notification(models.Model): date_created = models.DateTimeField(auto_now_add=True) def poster(self): + cached_username = getattr(self, "cached_poster", None) + if cached_username is not None: + return cached_username user = User.objects.get(id=self.pid) - return user.username + username = user.username + self.cached_poster = username + return username # CDEEP database created using inspectdb arg of manage.py diff --git a/website/templatetags/count_tags.py b/website/templatetags/count_tags.py index f5c3531..0f70862 100755 --- a/website/templatetags/count_tags.py +++ b/website/templatetags/count_tags.py @@ -6,10 +6,16 @@ register = template.Library() -# Counts the number of questions in +# Counts the number of questions in , cached for faster rendering def category_question_count(category): - category_question_count = Question.objects.filter(category=category).count() - return category_question_count + cache_key = f"stats:category_question_count:{category}" + count = cache.get(cache_key) + if count is not None: + return count + + count = Question.objects.filter(category=category).count() + cache.set(cache_key, count, 300) + return count register.simple_tag(category_question_count) diff --git a/website/templatetags/notify.py b/website/templatetags/notify.py index 8da07f7..c2b4144 100755 --- a/website/templatetags/notify.py +++ b/website/templatetags/notify.py @@ -5,16 +5,16 @@ register = template.Library() -def get_notification(nid): - notification = Notification.objects.get(pk=nid) - try: - question = Question.objects.get(pk=notification.qid) - except Question.DoesNotExist: - question = None - try: - answer = Answer.objects.get(pk=notification.aid) - except Answer.DoesNotExist: - answer = None +def get_notification(notification): + """ + Render a single notification. + + The view (`user_notifications`) attaches `cached_question`, `cached_answer` + and `cached_poster` attributes on each `Notification` instance so that this + tag does not need to perform any additional database queries. + """ + question = getattr(notification, "cached_question", None) + answer = getattr(notification, "cached_answer", None) context = { 'notification': notification, 'question': question, diff --git a/website/templatetags/sidebar_tags.py b/website/templatetags/sidebar_tags.py index a175052..e000888 100755 --- a/website/templatetags/sidebar_tags.py +++ b/website/templatetags/sidebar_tags.py @@ -1,4 +1,5 @@ from django import template +from django.db.models import Count from website.models import Question @@ -6,8 +7,16 @@ def recent_questions(): - recent_questions = Question.objects.all().order_by('-id')[:5] - return {'recent_questions': recent_questions} + recent_questions = ( + Question.objects.all() + .annotate(total_answers=Count('answer')) + .order_by('-id')[:5] + ) + return { + 'questions': recent_questions, + 'total': 10, + 'marker': 0, + } -register.inclusion_tag('website/templates/recent_questions.html')(recent_questions) +register.inclusion_tag('website/templates/recent-questions.html')(recent_questions) diff --git a/website/views.py b/website/views.py index 62f8bd4..01b0116 100755 --- a/website/views.py +++ b/website/views.py @@ -167,8 +167,7 @@ def home(request): def questions(request): - questions = Question.objects.filter(status=1).order_by('category', 'tutorial') - questions = questions.annotate(total_answers=Count('answer')) + questions = Question.objects.filter(status=1).annotate(total_answers=Count('answer')).order_by('category', 'tutorial') raw_get_data = request.GET.get('o', None) @@ -194,16 +193,22 @@ def questions(request): questions = paginator.page(1) except EmptyPage: questions = paginator.page(paginator.num_pages) + # Attach cached usernames to avoid N+1 in templates + uids = {q.uid for q in questions} + users = {u.id: u.username for u in User.objects.filter(id__in=uids)} + for q in questions: + q.cached_user = users.get(q.uid, "Unknown User") + context = { 'questions': questions, 'header': header, - 'ordering': ordering - } + 'ordering': ordering, + } return render(request, 'website/templates/questions.html', context) def hidden_questions(request): - questions = Question.objects.filter(status=0).order_by('date_created').reverse() + questions = Question.objects.filter(status=0).annotate(total_answers=Count('answer')).order_by('-date_created') paginator = Paginator(questions, 20) page = request.GET.get('page') @@ -213,8 +218,14 @@ def hidden_questions(request): questions = paginator.page(1) except EmptyPage: questions = paginator.page(paginator.num_pages) + # Attach cached usernames similar to questions() view + uids = {q.uid for q in questions} + users = {u.id: u.username for u in User.objects.filter(id__in=uids)} + for q in questions: + q.cached_user = users.get(q.uid, "Unknown User") + context = { - 'questions': questions + 'questions': questions, } return render(request, 'website/templates/questions.html', context) @@ -225,19 +236,39 @@ def get_question(request, question_id=None, pretty_url=None): category = FossCategory.objects.all().order_by('foss') if pretty_url != pretty_title: return HttpResponseRedirect('/question/' + question_id + '/' + pretty_title) - answers = question.answer_set.all() + # Prefetch answers and their comments to avoid N+1 queries + answers = ( + question.answer_set.all() + .prefetch_related('answercomment_set') + .order_by('date_created') + ) form = AnswerQuesitionForm() if question.status in (0,2): label = "Show" else: label = "Hide" + # Cache usernames for question, answers and comments + uids = {question.uid} + for answer in answers: + uids.add(answer.uid) + for comment in answer.answercomment_set.all(): + uids.add(comment.uid) + + users = {u.id: u.username for u in User.objects.filter(id__in=uids)} + + question.cached_user = users.get(question.uid, "Unknown User") + for answer in answers: + answer.cached_user = users.get(answer.uid, "Unknown User") + for comment in answer.answercomment_set.all(): + comment.cached_user = users.get(comment.uid, "Unknown User") + context = { 'question': question, 'answers': answers, 'category': category, 'form': form, - 'label': label + 'label': label, } user_has_role = has_role(request.user) context['require_recaptcha'] = not user_has_role @@ -657,7 +688,11 @@ def user_answers(request, user_id): if str(user_id) == str(request.user.id): total = Answer.objects.filter(uid=user_id).count() total = int(total - (total % 10 - 10)) - answers = Answer.objects.filter(uid=user_id).order_by('date_created').reverse()[marker:marker + 10] + answers = ( + Answer.objects.filter(uid=user_id) + .select_related('question') + .order_by('-date_created')[marker:marker + 10] + ) context = { 'answers': answers, 'total': total, @@ -670,9 +705,26 @@ def user_answers(request, user_id): @login_required def user_notifications(request, user_id): if str(user_id) == str(request.user.id): - notifications = Notification.objects.filter(uid=user_id).order_by('date_created').reverse() + notifications = list( + Notification.objects.filter(uid=user_id).order_by('-date_created') + ) + + # Prefetch related questions, answers and posters in bulk + qids = {n.qid for n in notifications if n.qid} + aids = {n.aid for n in notifications if n.aid} + pids = {n.pid for n in notifications if n.pid} + + questions = {q.id: q for q in Question.objects.filter(id__in=qids)} + answers = {a.id: a for a in Answer.objects.filter(id__in=aids)} + users = {u.id: u.username for u in User.objects.filter(id__in=pids)} + + for n in notifications: + n.cached_question = questions.get(n.qid) + n.cached_answer = answers.get(n.aid) + n.cached_poster = users.get(n.pid, "Unknown User") + context = { - 'notifications': notifications + 'notifications': notifications, } return render(request, 'website/templates/notifications.html', context) return HttpResponse("go away ...") @@ -836,13 +888,22 @@ def ajax_similar_questions(request): user_title = clean_user_data(title) # Increase the threshold as the Forums questions increase THRESHOLD = 0.3 + MAX_CANDIDATES = 200 + MAX_RESULTS = 20 + top_ques = [] - questions = Question.objects.filter(category=category,tutorial=tutorial) + # Limit number of candidate questions to keep CPU usage bounded + questions = Question.objects.filter( + category=category, + tutorial=tutorial, + ).order_by('-date_created')[:MAX_CANDIDATES] + for question in questions: - question.similarity= get_similar_questions(user_title,question.title) - if question.similarity >= THRESHOLD: + question.similarity = get_similar_questions(user_title, question.title) + if question.similarity >= THRESHOLD: top_ques.append(question) - top_ques = sorted(top_ques,key=lambda x : x.similarity, reverse=True) + + top_ques = sorted(top_ques, key=lambda x: x.similarity, reverse=True)[:MAX_RESULTS] context = { 'questions': top_ques, 'questions_count':len(top_ques) @@ -895,18 +956,23 @@ def ajax_keyword_search(request): questions = Question.objects.filter( Q(title__icontains=key) | Q(category__icontains=key) | Q(tutorial__icontains=key) | Q(body__icontains=key), status=1 - ).order_by('-date_created') + ).annotate(total_answers=Count('answer')).order_by('-date_created') + paginator = Paginator(questions, 20) page = request.POST.get('page') - if page: - page = int(request.POST.get('page')) - questions = paginator.page(page) try: questions = paginator.page(page) except PageNotAnInteger: questions = paginator.page(1) except EmptyPage: questions = paginator.page(paginator.num_pages) + + # Attach cached usernames for the current page + uids = {q.uid for q in questions} + users = {u.id: u.username for u in User.objects.filter(id__in=uids)} + for q in questions: + q.cached_user = users.get(q.uid, "Unknown User") + context = { 'questions': questions } @@ -919,19 +985,38 @@ def ajax_time_search(request): tutorial = request.POST.get('tutorial') minute_range = request.POST.get('minute_range') second_range = request.POST.get('second_range') - questions = None + questions = Question.objects.none() if category: - questions = Question.objects.filter(category=category.replace(' ', '-'), status=1) + questions = Question.objects.filter( + category=category.replace(' ', '-'), + status=1, + ) if tutorial: questions = questions.filter(tutorial=tutorial.replace(' ', '-')) if minute_range: - questions = questions.filter(category=category.replace( - ' ', '-'), tutorial=tutorial.replace(' ', '-'), minute_range=minute_range) + questions = questions.filter(minute_range=minute_range) if second_range: - questions = questions.filter(category=category.replace( - ' ', '-'), tutorial=tutorial.replace(' ', '-'), second_range=second_range) + questions = questions.filter(second_range=second_range) + + questions = questions.annotate(total_answers=Count('answer')).order_by('-date_created') + + paginator = Paginator(questions, 20) + page = request.POST.get('page') + try: + questions = paginator.page(page) + except PageNotAnInteger: + questions = paginator.page(1) + except EmptyPage: + questions = paginator.page(paginator.num_pages) + + # Attach cached usernames for the current page + uids = {q.uid for q in questions} + users = {u.id: u.username for u in User.objects.filter(id__in=uids)} + for q in questions: + q.cached_user = users.get(q.uid, "Unknown User") + context = { - 'questions': questions + 'questions': questions, } return render(request, 'website/templates/ajax-time-search.html', context) @@ -956,27 +1041,30 @@ def forums_mail(to='', subject='', message=''): def unanswered_notification(request): - questions = Question.objects.filter(status=1) + unanswered_questions = ( + Question.objects.filter(status=1) + .annotate(answer_count=Count('answer')) + .filter(answer_count=0) + ) total_count = 0 message = """ The following questions are left unanswered. Please take a look at them.

""" - for question in questions: - if not question.answer_set.count(): - total_count += 1 - message += """ - #{0}
- Title: {1}
- Category: {2}
- Link: {3}
-
- """.format( - total_count, - question.title, - question.category, - 'http://forums.spoken-tutorial.org/question/' + str(question.id) - ) + for question in unanswered_questions: + total_count += 1 + message += """ + #{0}
+ Title: {1}
+ Category: {2}
+ Link: {3}
+
+ """.format( + total_count, + question.title, + question.category, + 'http://forums.spoken-tutorial.org/question/' + str(question.id) + ) to = "team@spoken-tutorial.org, team@fossee.in" subject = "Unanswered questions in the forums." if total_count:
FOSS Tutorial - {{ question.views}} + {{ question.views }} - {{ question.answer_set.count }} + {{ question.total_answers }} - - {{ question.user|truncatechars:10 }} + + {{ question.cached_user|default:question.user|truncatechars:10 }}