Merge pull request #1793 from bookwyrm-social/more-caches

More caches
This commit is contained in:
Mouse Reeve 2022-01-10 11:25:26 -08:00 committed by GitHub
commit 4ba375892a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 125 additions and 74 deletions

View file

@ -1,5 +1,6 @@
""" progress in a book """ """ progress in a book """
from django.core import validators from django.core import validators
from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
@ -30,6 +31,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
self.user.update_active_date() self.user.update_active_date()
# an active readthrough must have an unset finish date # an active readthrough must have an unset finish date
if self.finish_date: if self.finish_date:

View file

@ -1,7 +1,6 @@
""" defines relationships between users """ """ defines relationships between users """
from django.apps import apps from django.apps import apps
from django.core.cache import cache from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import Q from django.db.models import Q
@ -41,15 +40,12 @@ class UserRelationship(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""clear the template cache""" """clear the template cache"""
# invalidate the template cache # invalidate the template cache
cache_keys = [ cache.delete_many(
make_template_fragment_key( [
"follow_button", [self.user_subject.id, self.user_object.id] f"relationship-{self.user_subject.id}-{self.user_object.id}",
), f"relationship-{self.user_object.id}-{self.user_subject.id}",
make_template_fragment_key( ]
"follow_button", [self.user_object.id, self.user_subject.id] )
),
]
cache.delete_many(cache_keys)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:

View file

@ -1,13 +1,17 @@
{% extends 'landing/layout.html' %} {% extends 'landing/layout.html' %}
{% load i18n %} {% load i18n %}
{% load cache %} {% load cache %}
{% load bookwyrm_tags %}
{% block panel %} {% block panel %}
<div class="block is-hidden-tablet"> <div class="block is-hidden-tablet">
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2> <h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</div> </div>
{% cache 60 * 60 %} {% get_current_language as LANGUAGE_CODE %}
{% cache 60 * 60 LANGUAGE_CODE %}
{% get_landing_books as books %}
<section class="tile is-ancestor"> <section class="tile is-ancestor">
<div class="tile is-vertical is-6"> <div class="tile is-vertical is-6">
<div class="tile is-parent"> <div class="tile is-parent">

View file

@ -1,13 +1,18 @@
{% load i18n %} {% load i18n %}
{% load interaction %}
{% if request.user == user or not request.user.is_authenticated %} {% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {# nothing to see here -- either it's yourself or your logged out #}
{% else %}
{% get_relationship user as relationship %}
{% if relationship.is_blocked %}
{% include 'snippets/block_button.html' with blocks=True %} {% include 'snippets/block_button.html' with blocks=True %}
{% else %} {% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}"> <div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
<div class="control"> <div class="control">
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if request.user in user.followers.all or request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if relationship.is_following or relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit"> <button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit">
@ -18,10 +23,10 @@
{% endif %} {% endif %}
</button> </button>
</form> </form>
<form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not request.user in user.followers.all and not request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not relationship.is_following and not relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers and request.user not in user.followers.all %} {% if user.manually_approves_followers and not relationship.is_following %}
<button class="button is-small is-danger is-light" type="submit"> <button class="button is-small is-danger is-light" type="submit">
{% trans "Undo follow request" %} {% trans "Undo follow request" %}
</button> </button>
@ -42,4 +47,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endif %} {% endif %}

View file

@ -3,6 +3,7 @@ from django import template
from django.db.models import Avg, StdDev, Count, F, Q from django.db.models import Avg, StdDev, Count, F, Q
from bookwyrm import models from bookwyrm import models
from bookwyrm.utils import cache
from bookwyrm.views.feed import get_suggested_books from bookwyrm.views.feed import get_suggested_books
@ -130,35 +131,53 @@ def related_status(notification):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_shelf(context, book): def active_shelf(context, book):
"""check what shelf a user has a book on, if any""" """check what shelf a user has a book on, if any"""
if hasattr(book, "current_shelves"): user = context["request"].user
read_shelves = [ return cache.get_or_set(
s f"active_shelf-{user.id}-{book.id}",
for s in book.current_shelves lambda u, b: (
if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS models.ShelfBook.objects.filter(
] shelf__user=u,
return read_shelves[0] if len(read_shelves) else {"book": book} book__parent_work__editions=b,
).first()
shelf = (
models.ShelfBook.objects.filter(
shelf__user=context["request"].user,
book__parent_work__editions=book,
) )
.select_related("book", "shelf") or {"book": book},
.first() user,
book,
timeout=15552000,
) )
return shelf if shelf else {"book": book}
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def latest_read_through(book, user): def latest_read_through(book, user):
"""the most recent read activity""" """the most recent read activity"""
if hasattr(book, "active_readthroughs"): return cache.get_or_set(
return book.active_readthroughs[0] if len(book.active_readthroughs) else None f"latest_read_through-{user.id}-{book.id}",
lambda u, b: (
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date")
.first()
),
user,
book,
timeout=15552000,
)
return (
models.ReadThrough.objects.filter(user=user, book=book, is_active=True) @register.simple_tag(takes_context=False)
.order_by("-start_date") def get_landing_books():
.first() """list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
) )

View file

@ -1,8 +1,8 @@
""" template filters for status interaction buttons """ """ template filters for status interaction buttons """
from django import template from django import template
from django.core.cache import cache
from bookwyrm import models from bookwyrm import models
from bookwyrm.utils.cache import get_or_set
register = template.Library() register = template.Library()
@ -11,20 +11,23 @@ register = template.Library()
@register.filter(name="liked") @register.filter(name="liked")
def get_user_liked(user, status): def get_user_liked(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return cache.get_or_set( return get_or_set(
f"fav-{user.id}-{status.id}", f"fav-{user.id}-{status.id}",
models.Favorite.objects.filter(user=user, status=status).exists(), lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(),
259200, user,
status,
timeout=259200,
) )
@register.filter(name="boosted") @register.filter(name="boosted")
def get_user_boosted(user, status): def get_user_boosted(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return cache.get_or_set( return get_or_set(
f"boost-{user.id}-{status.id}", f"boost-{user.id}-{status.id}",
status.boosters.filter(user=user).exists(), lambda u: status.boosters.filter(user=u).exists(),
259200, user,
timeout=259200,
) )
@ -32,3 +35,32 @@ def get_user_boosted(user, status):
def get_user_saved_lists(user, book_list): def get_user_saved_lists(user, book_list):
"""did the user save a list""" """did the user save a list"""
return user.saved_lists.filter(id=book_list.id).exists() return user.saved_lists.filter(id=book_list.id).exists()
@register.simple_tag(takes_context=True)
def get_relationship(context, user_object):
"""caches the relationship between the logged in user and another user"""
user = context["request"].user
return get_or_set(
f"relationship-{user.id}-{user_object.id}",
get_relationship_name,
user,
user_object,
timeout=259200,
)
def get_relationship_name(user, user_object):
"""returns the relationship type"""
types = {
"is_following": False,
"is_follow_pending": False,
"is_blocked": False,
}
if user_object in user.blocks.all():
types["is_blocked"] = True
elif user_object in user.following.all():
types["is_following"] = True
elif user_object in user.follower_requests.all():
types["is_follow_pending"] = True
return types

11
bookwyrm/utils/cache.py Normal file
View file

@ -0,0 +1,11 @@
""" Custom handler for caching """
from django.core.cache import cache
def get_or_set(cache_key, function, *args, timeout=None):
"""Django's built-in get_or_set isn't cutting it"""
value = cache.get(cache_key)
if value is None:
value = function(*args)
cache.set(cache_key, value, timeout=timeout)
return value

View file

@ -16,7 +16,6 @@ from django.views.decorators.http import require_POST
from bookwyrm import emailing, forms, models from bookwyrm import emailing, forms, models
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views import helpers
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -174,7 +173,6 @@ class InviteRequest(View):
data = { data = {
"request_form": form, "request_form": form,
"request_received": received, "request_received": received,
"books": helpers.get_landing_books(),
} }
return TemplateResponse(request, "landing/landing.html", data) return TemplateResponse(request, "landing/landing.html", data)

View file

@ -153,24 +153,6 @@ def is_blocked(viewer, user):
return False return False
def get_landing_books():
"""list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
)
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime: def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
"""ensures that data is stored consistently in the UTC timezone""" """ensures that data is stored consistently in the UTC timezone"""
if not date_str: if not date_str:

View file

@ -3,7 +3,6 @@ from django.template.response import TemplateResponse
from django.views import View from django.views import View
from bookwyrm import forms from bookwyrm import forms
from bookwyrm.views import helpers
from bookwyrm.views.feed import Feed from bookwyrm.views.feed import Feed
@ -28,6 +27,5 @@ class Landing(View):
data = { data = {
"register_form": forms.RegisterForm(), "register_form": forms.RegisterForm(),
"request_form": forms.InviteRequestForm(), "request_form": forms.InviteRequestForm(),
"books": helpers.get_landing_books(),
} }
return TemplateResponse(request, "landing/landing.html", data) return TemplateResponse(request, "landing/landing.html", data)

View file

@ -1,7 +1,6 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.cache import cache from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import transaction from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -46,12 +45,10 @@ class ReadingStatus(View):
if not identifier: if not identifier:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# invalidate the template cache # invalidate related caches
cache_keys = [ cache.delete(
make_template_fragment_key("shelve_button", [request.user.id, book_id]), f"active_shelf-{request.user.id}-{book_id}",
make_template_fragment_key("suggested_books", [request.user.id]), )
]
cache.delete_many(cache_keys)
desired_shelf = get_object_or_404( desired_shelf = get_object_or_404(
models.Shelf, identifier=identifier, user=request.user models.Shelf, identifier=identifier, user=request.user

View file

@ -6,13 +6,18 @@ markers =
integration: marks tests as requiring external resources (deselect with '-m "not integration"') integration: marks tests as requiring external resources (deselect with '-m "not integration"')
env = env =
SECRET_KEY = beepbeep
DEBUG = false DEBUG = false
USE_HTTPS=true USE_HTTPS = true
DOMAIN = your.domain.here DOMAIN = your.domain.here
BOOKWYRM_DATABASE_BACKEND = postgres BOOKWYRM_DATABASE_BACKEND = postgres
MEDIA_ROOT = images/ MEDIA_ROOT = images/
CELERY_BROKER = "" CELERY_BROKER = ""
REDIS_BROKER_PORT = 6379 REDIS_BROKER_PORT = 6379
REDIS_BROKER_PASSWORD = beep
REDIS_ACTIVITY_PORT = 6379
REDIS_ACTIVITY_PASSWORD = beep
USE_DUMMY_CACHE = true
FLOWER_PORT = 8888 FLOWER_PORT = 8888
EMAIL_HOST = "smtp.mailgun.org" EMAIL_HOST = "smtp.mailgun.org"
EMAIL_PORT = 587 EMAIL_PORT = 587
@ -20,4 +25,3 @@ env =
EMAIL_HOST_PASSWORD = "" EMAIL_HOST_PASSWORD = ""
EMAIL_USE_TLS = true EMAIL_USE_TLS = true
ENABLE_PREVIEW_IMAGES = false ENABLE_PREVIEW_IMAGES = false
USE_S3 = false