diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index f75918ac..ceb8e0b6 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -1,5 +1,6 @@ """ progress in a book """ from django.core import validators +from django.core.cache import cache from django.db import models from django.db.models import F, Q @@ -30,6 +31,7 @@ class ReadThrough(BookWyrmModel): def save(self, *args, **kwargs): """update user active time""" + cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}") self.user.update_active_date() # an active readthrough must have an unset finish date if self.finish_date: diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 03417454..e95c38fa 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,7 +1,6 @@ """ defines relationships between users """ from django.apps import apps 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.models import Q @@ -41,15 +40,12 @@ class UserRelationship(BookWyrmModel): def save(self, *args, **kwargs): """clear the template cache""" # invalidate the template cache - cache_keys = [ - make_template_fragment_key( - "follow_button", [self.user_subject.id, self.user_object.id] - ), - make_template_fragment_key( - "follow_button", [self.user_object.id, self.user_subject.id] - ), - ] - cache.delete_many(cache_keys) + cache.delete_many( + [ + f"relationship-{self.user_subject.id}-{self.user_object.id}", + f"relationship-{self.user_object.id}-{self.user_subject.id}", + ] + ) super().save(*args, **kwargs) class Meta: diff --git a/bookwyrm/templates/landing/landing.html b/bookwyrm/templates/landing/landing.html index 759e8c61..c3771759 100644 --- a/bookwyrm/templates/landing/landing.html +++ b/bookwyrm/templates/landing/landing.html @@ -1,13 +1,17 @@ {% extends 'landing/layout.html' %} {% load i18n %} {% load cache %} +{% load bookwyrm_tags %} + {% block panel %}

{% trans "Recent Books" %}

-{% cache 60 * 60 %} +{% get_current_language as LANGUAGE_CODE %} +{% cache 60 * 60 LANGUAGE_CODE %} +{% get_landing_books as books %}
diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index f7025bba..0482bde0 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -1,13 +1,18 @@ {% load i18n %} +{% load interaction %} {% 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 %} {% else %}
- -
{% endif %}
+ +{% endif %} + {% endif %} diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index f24219d2..3c57fb27 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -3,6 +3,7 @@ from django import template from django.db.models import Avg, StdDev, Count, F, Q from bookwyrm import models +from bookwyrm.utils import cache from bookwyrm.views.feed import get_suggested_books @@ -130,35 +131,53 @@ 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""" - if hasattr(book, "current_shelves"): - read_shelves = [ - s - for s in book.current_shelves - if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS - ] - return read_shelves[0] if len(read_shelves) else {"book": book} - - shelf = ( - models.ShelfBook.objects.filter( - shelf__user=context["request"].user, - book__parent_work__editions=book, + user = context["request"].user + return cache.get_or_set( + f"active_shelf-{user.id}-{book.id}", + lambda u, b: ( + models.ShelfBook.objects.filter( + shelf__user=u, + book__parent_work__editions=b, + ).first() ) - .select_related("book", "shelf") - .first() + or {"book": book}, + user, + book, + timeout=15552000, ) - return shelf if shelf else {"book": book} @register.simple_tag(takes_context=False) def latest_read_through(book, user): """the most recent read activity""" - if hasattr(book, "active_readthroughs"): - return book.active_readthroughs[0] if len(book.active_readthroughs) else None + return cache.get_or_set( + 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) - .order_by("-start_date") - .first() + +@register.simple_tag(takes_context=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] + ) ) diff --git a/bookwyrm/templatetags/interaction.py b/bookwyrm/templatetags/interaction.py index 90309aaf..89a25420 100644 --- a/bookwyrm/templatetags/interaction.py +++ b/bookwyrm/templatetags/interaction.py @@ -1,8 +1,8 @@ """ template filters for status interaction buttons """ from django import template -from django.core.cache import cache from bookwyrm import models +from bookwyrm.utils.cache import get_or_set register = template.Library() @@ -11,20 +11,23 @@ register = template.Library() @register.filter(name="liked") def get_user_liked(user, status): """did the given user fav a status?""" - return cache.get_or_set( + return get_or_set( f"fav-{user.id}-{status.id}", - models.Favorite.objects.filter(user=user, status=status).exists(), - 259200, + lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(), + user, + status, + timeout=259200, ) @register.filter(name="boosted") def get_user_boosted(user, status): """did the given user fav a status?""" - return cache.get_or_set( + return get_or_set( f"boost-{user.id}-{status.id}", - status.boosters.filter(user=user).exists(), - 259200, + lambda u: status.boosters.filter(user=u).exists(), + user, + timeout=259200, ) @@ -32,3 +35,32 @@ def get_user_boosted(user, status): def get_user_saved_lists(user, book_list): """did the user save a list""" 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 diff --git a/bookwyrm/utils/cache.py b/bookwyrm/utils/cache.py new file mode 100644 index 00000000..aebb8e75 --- /dev/null +++ b/bookwyrm/utils/cache.py @@ -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 diff --git a/bookwyrm/views/admin/invite.py b/bookwyrm/views/admin/invite.py index 8fd68b70..322c5fcb 100644 --- a/bookwyrm/views/admin/invite.py +++ b/bookwyrm/views/admin/invite.py @@ -16,7 +16,6 @@ from django.views.decorators.http import require_POST from bookwyrm import emailing, forms, models from bookwyrm.settings import PAGE_LENGTH -from bookwyrm.views import helpers # pylint: disable= no-self-use @@ -174,7 +173,6 @@ class InviteRequest(View): data = { "request_form": form, "request_received": received, - "books": helpers.get_landing_books(), } return TemplateResponse(request, "landing/landing.html", data) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 8cc0aea8..74d867b6 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -153,24 +153,6 @@ def is_blocked(viewer, user): 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: """ensures that data is stored consistently in the UTC timezone""" if not date_str: diff --git a/bookwyrm/views/landing/landing.py b/bookwyrm/views/landing/landing.py index c1db28c7..cfd9b48b 100644 --- a/bookwyrm/views/landing/landing.py +++ b/bookwyrm/views/landing/landing.py @@ -3,7 +3,6 @@ from django.template.response import TemplateResponse from django.views import View from bookwyrm import forms -from bookwyrm.views import helpers from bookwyrm.views.feed import Feed @@ -28,6 +27,5 @@ class Landing(View): data = { "register_form": forms.RegisterForm(), "request_form": forms.InviteRequestForm(), - "books": helpers.get_landing_books(), } return TemplateResponse(request, "landing/landing.html", data) diff --git a/bookwyrm/views/reading.py b/bookwyrm/views/reading.py index c7eda10e..fd12dc0f 100644 --- a/bookwyrm/views/reading.py +++ b/bookwyrm/views/reading.py @@ -1,7 +1,6 @@ """ the good stuff! the books! """ from django.contrib.auth.decorators import login_required from django.core.cache import cache -from django.core.cache.utils import make_template_fragment_key from django.db import transaction from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect @@ -46,12 +45,10 @@ class ReadingStatus(View): if not identifier: return HttpResponseBadRequest() - # invalidate the template cache - cache_keys = [ - make_template_fragment_key("shelve_button", [request.user.id, book_id]), - make_template_fragment_key("suggested_books", [request.user.id]), - ] - cache.delete_many(cache_keys) + # invalidate related caches + cache.delete( + f"active_shelf-{request.user.id}-{book_id}", + ) desired_shelf = get_object_or_404( models.Shelf, identifier=identifier, user=request.user diff --git a/pytest.ini b/pytest.ini index 9ef72449..c5cdc35d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -6,13 +6,18 @@ markers = integration: marks tests as requiring external resources (deselect with '-m "not integration"') env = + SECRET_KEY = beepbeep DEBUG = false - USE_HTTPS=true + USE_HTTPS = true DOMAIN = your.domain.here BOOKWYRM_DATABASE_BACKEND = postgres MEDIA_ROOT = images/ CELERY_BROKER = "" REDIS_BROKER_PORT = 6379 + REDIS_BROKER_PASSWORD = beep + REDIS_ACTIVITY_PORT = 6379 + REDIS_ACTIVITY_PASSWORD = beep + USE_DUMMY_CACHE = true FLOWER_PORT = 8888 EMAIL_HOST = "smtp.mailgun.org" EMAIL_PORT = 587 @@ -20,4 +25,3 @@ env = EMAIL_HOST_PASSWORD = "" EMAIL_USE_TLS = true ENABLE_PREVIEW_IMAGES = false - USE_S3 = false