Merge pull request #1122 from bookwyrm-social/feed-page-queries

Improves query efficiency for feed page
This commit is contained in:
Mouse Reeve 2021-05-22 22:11:33 -07:00 committed by GitHub
commit de5893e7fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 123 additions and 70 deletions

View file

@ -55,6 +55,8 @@ class ActivityStream(RedisStore):
return (
models.Status.objects.select_subclasses()
.filter(id__in=statuses)
.select_related("user", "reply_parent")
.prefetch_related("mention_books", "mention_users")
.order_by("-published_date")
)

View file

@ -381,17 +381,16 @@ class AnnualGoal(BookWyrmModel):
return {r.book.id: r.rating for r in reviews}
@property
def progress_percent(self):
"""how close to your goal, in percent form"""
return int(float(self.book_count / self.goal) * 100)
@property
def book_count(self):
def progress(self):
"""how many books you've read this year"""
return self.user.readthrough_set.filter(
count = self.user.readthrough_set.filter(
finish_date__year__gte=self.year,
finish_date__year__lt=self.year + 1,
).count()
return {
"count": count,
"percent": int(float(count / self.goal) * 100),
}
@app.task

View file

@ -1,7 +1,7 @@
{% extends 'layout.html' %}
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}
{% block title %}{{ book.title }}{% endblock %}
{% block title %}{{ book|title }}{% endblock %}
{% block content %}
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
@ -137,7 +137,7 @@
{# user's relationship to the book #}
<div class="block">
{% for shelf in user_shelves %}
{% for shelf in user_shelfbooks %}
<p>
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}This edition is on your <a href="{{ path }}">{{ shelf_name }}</a> shelf.{% endblocktrans %}
{% include 'snippets/shelf_selector.html' with current=shelf.shelf %}

View file

@ -5,7 +5,7 @@
<div class="select is-small mt-1 mb-3">
<select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">
<option disabled selected value>Add to your books</option>
{% for shelf in request.user.shelf_set.all %}
{% for shelf in user_shelves %}
<option value="{{ shelf.id }}">{{ shelf.name }}</option>
{% endfor %}
</select>

View file

@ -134,12 +134,14 @@
<span class="is-sr-only">{% trans "Notifications" %}</span>
</span>
</span>
{% with request.user.unread_notification_count as notification_count %}
<span
class="{% if not request.user.unread_notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
class="{% if not notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
data-poll-wrapper
>
<span data-poll="notifications">{{ request.user.unread_notification_count }}</span>
<span data-poll="notifications">{{ notification_count }}</span>
</span>
{% endwith %}
</a>
</div>
{% else %}
@ -193,8 +195,11 @@
<div class="section is-flex-grow-1">
<div class="container">
{# almost every view needs to know the user shelves #}
{% with request.user.shelf_set.all as user_shelves %}
{% block content %}
{% endblock %}
{% endwith %}
</div>
</div>

View file

@ -18,7 +18,7 @@
<a href="{{ user.local_path }}">{% include 'snippets/avatar.html' with user=user %} {{ user.display_name }}</a>
</p>
<p class="mr-2">
{% include 'snippets/block_button.html' with user=user %}
{% include 'snippets/block_button.html' with user=user blocks=True %}
</p>
</li>
{% endfor %}

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% if not user in request.user.blocks.all %}
{% if not blocks %}
<form name="blocks" method="post" action="/block/{{ user.id }}">
{% csrf_token %}
<button class="button is-danger is-light is-small {{ class }}" type="submit">{% trans "Block" %}</button>

View file

@ -3,14 +3,31 @@
{% load i18n %}
{% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% with request.user|boosted:status as boosted %}
<form
name="boost"
action="/boost/{{ status.id }}"
method="post"
class="interaction boost-{{ status.id }}-{{ uuid }} {% if boosted %}is-hidden{% endif %}"
data-id="boost-{{ status.id }}-{{ uuid }}"
>
{% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit" {% if not status.boostable %}disabled{% endif %}>
<button
class="button is-small is-light is-transparent"
type="submit"
{% if not status.boostable %}disabled{% endif %}
>
<span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span>
<span class="is-sr-only-mobile">{% trans "Boost" %}</span>
</button>
</form>
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
<form
name="unboost"
action="/unboost/{{ status.id }}"
method="post"
class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not boosted %}is-hidden{% endif %}"
data-id="boost-{{ status.id }}-{{ uuid }}"
>
{% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span>
@ -18,3 +35,4 @@
</button>
</form>
{% endwith %}
{% endwith %}

View file

@ -3,7 +3,8 @@
{% load i18n %}
{% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% with request.user|liked:status as liked %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if liked %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
@ -11,7 +12,7 @@
<span class="is-sr-only-mobile">{% trans "Like" %}</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not liked %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-light is-transparent is-small" type="submit">
<span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>
@ -19,3 +20,4 @@
</button>
</form>
{% endwith %}
{% endwith %}

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' %}
{% include 'snippets/block_button.html' with blocks=True %}
{% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">

View file

@ -1,16 +1,19 @@
{% load i18n %}
{% load humanize %}
{% with goal.progress as progress %}
<p>
{% if goal.progress_percent >= 100 %}
{% if progress.percent >= 100 %}
{% trans "Success!" %}
{% elif goal.progress_percent %}
{% blocktrans with percent=goal.progress_percent %}{{ percent }}% complete!{% endblocktrans %}
{% elif progress.percent %}
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
{% endif %}
{% if goal.user == request.user %}
{% blocktrans with read_count=goal.book_count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}You've read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% blocktrans with read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}You've read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% else %}
{% blocktrans with username=goal.user.display_name read_count=goal.book_count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% endif %}
</p>
<progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress>
<progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
{% endwith %}

View file

@ -6,7 +6,7 @@
{% endblock %}
{% block dropdown-list %}
{% for shelf in request.user.shelf_set.all %}
{% for shelf in user_shelves %}
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}

View file

@ -13,7 +13,7 @@
</div>
{% else %}
<div class="control">
{% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %}
{% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=user_shelves active_shelf=active_shelf button_uuid=uuid %}
</div>
{% include 'snippets/shelve_button/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%}
{% endif %}
@ -25,7 +25,7 @@
{% include 'snippets/shelve_button/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/shelve_button/progress_update_modal.html' with book=shelf_book.book controls_text="progress-update" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/shelve_button/progress_update_modal.html' with book=active_shelf_book.book controls_text="progress-update" controls_uid=uuid readthrough=readthrough %}
{% endwith %}
{% endif %}

View file

@ -7,5 +7,5 @@
{% endblock %}
{% block dropdown-list %}
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=user_shelves dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
{% endblock %}

View file

@ -39,7 +39,7 @@
{% include 'snippets/report_button.html' with user=status.user status=status %}
</li>
<li role="menuitem" class="dropdown-item p-0">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" blocks=False %}
</li>
{% endif %}
{% endblock %}

View file

@ -16,6 +16,6 @@
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" blocks=False %}
</li>
{% endblock %}

View file

@ -3,8 +3,6 @@ from django import template
from django.db.models import Avg
from bookwyrm import models, views
from bookwyrm.views.status import to_markdown
from bookwyrm.templatetags.utilities import get_user_identifier
register = template.Library()
@ -71,9 +69,14 @@ def related_status(notification):
@register.simple_tag(takes_context=True)
def active_shelf(context, book):
"""check what shelf a user has a book on, if any"""
shelf = models.ShelfBook.objects.filter(
shelf__user=context["request"].user, book__in=book.parent_work.editions.all()
).first()
shelf = (
models.ShelfBook.objects.filter(
shelf__user=context["request"].user,
book__in=book.parent_work.editions.all(),
)
.select_related("book", "shelf")
.first()
)
return shelf if shelf else {"book": book}

View file

@ -9,14 +9,10 @@ register = template.Library()
@register.filter(name="liked")
def get_user_liked(user, status):
"""did the given user fav a status?"""
try:
models.Favorite.objects.get(user=user, status=status)
return True
except models.Favorite.DoesNotExist:
return False
return models.Favorite.objects.filter(user=user, status=status).exists()
@register.filter(name="boosted")
def get_user_boosted(user, status):
"""did the given user fav a status?"""
return user.id in status.boosters.all().values_list("user", flat=True)
return status.boosters.filter(user=user).exists()

View file

@ -42,7 +42,12 @@ def get_parent(status):
@register.filter(name="boosted_status")
def get_boosted(boost):
"""load a boosted status. have to do this or it won't get foreign keys"""
return models.Status.objects.select_subclasses().get(id=boost.boosted_status.id)
return (
models.Status.objects.select_subclasses()
.select_related("user", "reply_parent")
.prefetch_related("mention_books", "mention_users")
.get(id=boost.boosted_status.id)
)
@register.filter(name="published_date")

View file

@ -62,13 +62,16 @@ class Book(View):
queryset = queryset.filter(user=request.user)
else:
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
queryset = queryset.select_related("user")
paginated = Paginator(queryset, PAGE_LENGTH)
data = {
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content=""))
"ratings": reviews.filter(
Q(content__isnull=True) | Q(content="")
).select_related("user")
if not user_statuses
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
@ -89,15 +92,15 @@ class Book(View):
)
data["readthroughs"] = readthroughs
data["user_shelves"] = models.ShelfBook.objects.filter(
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
user=request.user, book=book
)
).select_related("shelf")
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
~Q(book=book),
user=request.user,
book__parent_work=book.parent_work,
)
).select_related("shelf", "book")
data["user_statuses"] = {
"review_count": book.review_set.filter(user=request.user).count(),

View file

@ -162,14 +162,15 @@ def get_suggested_books(user, max_books=5):
else max_books - book_count
)
shelf = user.shelf_set.get(identifier=preset)
shelf_books = shelf.shelfbook_set.order_by("-updated_date")[:limit]
if not shelf_books:
if not shelf.books.exists():
continue
shelf_preview = {
"name": shelf.name,
"identifier": shelf.identifier,
"books": [s.book for s in shelf_books],
"books": shelf.books.order_by("shelfbook").prefetch_related("authors")[
:limit
],
}
suggested_books.append(shelf_preview)
book_count += len(shelf_preview["books"])

View file

@ -13,6 +13,10 @@ from bookwyrm.utils import regex
def get_user_from_username(viewer, username):
"""helper function to resolve a localname or a username to a user"""
if viewer.is_authenticated and viewer.localname == username:
# that's yourself, fool
return viewer
# raises 404 if the user isn't found
try:
return models.User.viewer_aware_objects(viewer).get(localname=username)

View file

@ -2,7 +2,7 @@
from collections import namedtuple
from django.db import IntegrityError
from django.db.models import Count, OuterRef, Subquery, F, Q
from django.db.models import OuterRef, Subquery
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest, HttpResponseNotFound
@ -28,6 +28,11 @@ class Shelf(View):
"""display a shelf"""
user = get_user_from_username(request.user, username)
is_self = user == request.user
if is_self:
shelves = user.shelf_set
else:
shelves = privacy_filter(request.user, user.shelf_set)
# get the shelf and make sure the logged in user should be able to see it
@ -53,26 +58,28 @@ class Shelf(View):
if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET))
reviews = privacy_filter(
request.user,
models.Review.objects.filter(
reviews = models.Review.objects.filter(
user=user,
rating__isnull=False,
book__id=OuterRef("id"),
),
).order_by("-published_date")
books = books.annotate(rating=Subquery(reviews.values("rating")[:1]))
if not is_self:
reviews = privacy_filter(request.user, reviews)
books = books.annotate(
rating=Subquery(reviews.values("rating")[:1])
).prefetch_related("authors")
paginated = Paginator(
books.order_by("-updated_date"),
books.order_by("-shelfbook__updated_date"),
PAGE_LENGTH,
)
page = paginated.get_page(request.GET.get("page"))
data = {
"user": user,
"is_self": request.user == user,
"is_self": is_self,
"shelves": shelves.all(),
"shelf": shelf,
"books": page,

View file

@ -59,10 +59,15 @@ class User(View):
break
# user's posts
activities = privacy_filter(
activities = (
privacy_filter(
request.user,
user.status_set.select_subclasses(),
)
.select_related("reply_parent")
.prefetch_related("mention_books", "mention_users")
)
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year