Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2022-01-18 13:46:53 -08:00
commit 2dd39517c3
13 changed files with 145 additions and 29 deletions

View file

@ -342,6 +342,11 @@ class Edition(Book):
# set rank
self.edition_rank = self.get_rank()
# clear author cache
if self.id:
for author_id in self.authors.values_list("id", flat=True):
cache.delete(f"author-books-{author_id}")
return super().save(*args, **kwargs)
@classmethod

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs):
if not self.user:
self.user = self.shelf.user
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().delete(*args, **kwargs)
class Meta:
"""an opinionated constraint!
you can't put a book on shelf twice"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review
pure_type = "Article"
def save(self, *args, **kwargs):
"""clear rating caches"""
if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs)
class ReviewRating(Review):
"""a subtype of review that only contains a rating"""

View file

@ -106,6 +106,58 @@ TEMPLATES = [
},
]
LOG_LEVEL = env("LOG_LEVEL", "INFO").upper()
# Override aspects of the default handler to our taste
# See https://docs.djangoproject.com/en/3.2/topics/logging/#default-logging-configuration
# for a reference to the defaults we're overriding
#
# It seems that in order to override anything you have to include its
# entire dependency tree (handlers and filters) which makes this a
# bit verbose
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
# These are copied from the default configuration, required for
# implementing mail_admins below
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
},
"handlers": {
# Overrides the default handler to make it log to console
# regardless of the DEBUG setting (default is to not log to
# console if DEBUG=False)
"console": {
"level": LOG_LEVEL,
"class": "logging.StreamHandler",
},
# This is copied as-is from the default logger, and is
# required for the django section below
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
},
"loggers": {
# Install our new console handler for Django's logger, and
# override the log level while we're at it
"django": {
"handlers": ["console", "mail_admins"],
"level": LOG_LEVEL,
},
# Add a bookwyrm-specific logger
"bookwyrm": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
},
}
WSGI_APPLICATION = "bookwyrm.wsgi.application"

View file

@ -2,15 +2,15 @@
{% if rating %}
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path display_rating=rating|floatformat:"-1" review_title=name|safe count counter=rating %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} star): {{ review_title }}
Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}
{% plural %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} stars): {{ review_title }}
Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>": {{ review_title }}
Review of "{{ book_title }}": {{ review_title }}
{% endblocktrans %}
{% endif %}

View file

@ -6,8 +6,9 @@
{% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %}
{% latest_read_through book request.user as readthrough %}
{% with active_shelf_book=active_shelf.book %}
<div class="field has-addons mb-0 has-text-weight-normal" data-shelve-button-book="{{ book.id }}">
{% if switch_mode and active_shelf.book != book %}
{% if switch_mode and active_shelf_book != book %}
<div class="control">
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
</div>
@ -20,16 +21,17 @@
</div>
{% join "want_to_read" uuid as modal_id %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book id=modal_id class="" %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf_book id=modal_id class="" %}
{% join "start_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book id=modal_id class="" %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf_book id=modal_id class="" %}
{% join "finish_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% join "progress_update" uuid as modal_id %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% endwith %}
{% endwith %}
{% endif %}

View file

@ -38,7 +38,7 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if book|is_book_on_shelf:shelf %} disabled {% endif %}>
<span>{{ shelf.name }}</span>
</button>
</form>

View file

@ -45,7 +45,13 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<button
class="button {{ class }}"
name="shelf"
type="submit"
value="{{ shelf.identifier }}"
{% if book|is_book_on_shelf:shelf %} disabled {% endif %}
>
<span>{{ shelf.name }}</span>
</button>
</form>

View file

@ -13,10 +13,16 @@ register = template.Library()
@register.filter(name="rating")
def get_rating(book, user):
"""get the overall rating of a book"""
queryset = models.Review.privacy_filter(user).filter(
book__parent_work__editions=book
return cache.get_or_set(
f"book-rating-{book.parent_work.id}-{user.id}",
lambda u, b: models.Review.privacy_filter(u)
.filter(book__parent_work__editions=b)
.aggregate(Avg("rating"))["rating__avg"]
or 0,
user,
book,
timeout=15552000,
)
return queryset.aggregate(Avg("rating"))["rating__avg"]
@register.filter(name="user_rating")
@ -37,6 +43,18 @@ def get_user_rating(book, user):
return 0
@register.filter(name="is_book_on_shelf")
def get_is_book_on_shelf(book, shelf):
"""is a book on a shelf"""
return cache.get_or_set(
f"book-on-shelf-{book.id}-{shelf.id}",
lambda b, s: s.books.filter(id=b.id).exists(),
book,
shelf,
timeout=15552000,
)
@register.filter(name="book_description")
def get_book_description(book):
"""use the work's text if the book doesn't have it"""
@ -140,6 +158,7 @@ def active_shelf(context, book):
shelf__user=u,
book__parent_work__editions=b,
).first()
or False
),
user,
book,
@ -158,6 +177,7 @@ def latest_read_through(book, user):
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date")
.first()
or False
),
user,
book,

View file

@ -301,7 +301,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article")
self.assertEqual(
activity["name"],
f"Review of \"<a href='{self.book.local_path}'>{self.book.title}</a>\" (3 stars): Review's name",
f'Review of "{self.book.title}" (3 stars): Review\'s name',
)
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document")
@ -326,7 +326,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article")
self.assertEqual(
activity["name"],
f"Review of \"<a href='{self.book.local_path}'>{self.book.title}</a>\": Review name",
f'Review of "{self.book.title}": Review name',
)
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document")

View file

@ -12,6 +12,7 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.utils import cache
from bookwyrm.views.helpers import is_api_request
@ -30,14 +31,24 @@ class Author(View):
parent_work=OuterRef("parent_work")
).order_by("-edition_rank")
books = (
models.Edition.viewer_aware_objects(request.user)
.filter(Q(authors=author) | Q(parent_work__authors=author))
book_ids = cache.get_or_set(
f"author-books-{author.id}",
lambda a: models.Edition.objects.filter(
Q(authors=a) | Q(parent_work__authors=a)
)
.annotate(default_id=Subquery(default_editions.values("id")[:1]))
.filter(default_id=F("id"))
.order_by("-first_published_date", "-published_date", "-created_date")
.distinct()
.values_list("id", flat=True),
author,
timeout=15552000,
)
books = (
models.Edition.objects.filter(id__in=book_ids)
.order_by("-published_date", "-first_published_date", "-created_date")
.prefetch_related("authors")
).distinct()
)
paginated = Paginator(books, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))

View file

@ -1,7 +1,10 @@
""" incoming activities """
import json
import re
import logging
from urllib.parse import urldefrag
import requests
from django.http import HttpResponse, Http404
from django.core.exceptions import BadRequest, PermissionDenied
@ -9,13 +12,14 @@ from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
import requests
from bookwyrm import activitypub, models
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
from bookwyrm.utils import regex
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name="dispatch")
# pylint: disable=no-self-use
@ -71,6 +75,7 @@ def raise_is_blocked_user_agent(request):
return
url = url.group()
if models.FederatedServer.is_blocked(url):
logger.debug("%s is blocked, denying request based on user agent", url)
raise PermissionDenied()
@ -78,16 +83,18 @@ def raise_is_blocked_activity(activity_json):
"""get the sender out of activity json and check if it's blocked"""
actor = activity_json.get("actor")
# check if the user is banned/deleted
existing = models.User.find_existing_by_remote_id(actor)
if existing and existing.deleted:
raise PermissionDenied()
if not actor:
# well I guess it's not even a valid activity so who knows
return
# check if the user is banned/deleted
existing = models.User.find_existing_by_remote_id(actor)
if existing and existing.deleted:
logger.debug("%s is banned/deleted, denying request based on actor", actor)
raise PermissionDenied()
if models.FederatedServer.is_blocked(actor):
logger.debug("%s is blocked, denying request based on actor", actor)
raise PermissionDenied()

View file

@ -46,9 +46,7 @@ class ReadingStatus(View):
return HttpResponseBadRequest()
# invalidate related caches
cache.delete(
f"active_shelf-{request.user.id}-{book_id}",
)
cache.delete(f"active_shelf-{request.user.id}-{book_id}")
desired_shelf = get_object_or_404(
models.Shelf, identifier=identifier, user=request.user