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 # set rank
self.edition_rank = self.get_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) return super().save(*args, **kwargs)
@classmethod @classmethod

View file

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

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re import re
from django.apps import apps from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review activity_serializer = activitypub.Review
pure_type = "Article" 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): class ReviewRating(Review):
"""a subtype of review that only contains a rating""" """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" WSGI_APPLICATION = "bookwyrm.wsgi.application"

View file

@ -2,15 +2,15 @@
{% if rating %} {% 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 %} {% 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 %} {% 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 %} {% endblocktrans %}
{% else %} {% else %}
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %} {% 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 %} {% endblocktrans %}
{% endif %} {% endif %}

View file

@ -6,8 +6,9 @@
{% with book.id|uuid as uuid %} {% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %} {% active_shelf book as active_shelf %}
{% latest_read_through book request.user as readthrough %} {% 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 }}"> <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"> <div class="control">
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %} {% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
</div> </div>
@ -20,16 +21,17 @@
</div> </div>
{% join "want_to_read" uuid as modal_id %} {% 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 %} {% 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 %} {% 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 %} {% 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 %} {% endwith %}
{% endif %} {% endif %}

View file

@ -38,7 +38,7 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off"> <form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <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> <span>{{ shelf.name }}</span>
</button> </button>
</form> </form>

View file

@ -45,7 +45,13 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post"> <form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <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> <span>{{ shelf.name }}</span>
</button> </button>
</form> </form>

View file

@ -13,10 +13,16 @@ register = template.Library()
@register.filter(name="rating") @register.filter(name="rating")
def get_rating(book, user): def get_rating(book, user):
"""get the overall rating of a book""" """get the overall rating of a book"""
queryset = models.Review.privacy_filter(user).filter( return cache.get_or_set(
book__parent_work__editions=book 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") @register.filter(name="user_rating")
@ -37,6 +43,18 @@ def get_user_rating(book, user):
return 0 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") @register.filter(name="book_description")
def get_book_description(book): def get_book_description(book):
"""use the work's text if the book doesn't have it""" """use the work's text if the book doesn't have it"""
@ -140,6 +158,7 @@ def active_shelf(context, book):
shelf__user=u, shelf__user=u,
book__parent_work__editions=b, book__parent_work__editions=b,
).first() ).first()
or False
), ),
user, user,
book, book,
@ -158,6 +177,7 @@ def latest_read_through(book, user):
models.ReadThrough.objects.filter(user=u, book=b, is_active=True) models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date") .order_by("-start_date")
.first() .first()
or False
), ),
user, user,
book, book,

View file

@ -301,7 +301,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
activity["name"], 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["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
@ -326,7 +326,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
activity["name"], 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["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") 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.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.utils import cache
from bookwyrm.views.helpers import is_api_request from bookwyrm.views.helpers import is_api_request
@ -30,14 +31,24 @@ class Author(View):
parent_work=OuterRef("parent_work") parent_work=OuterRef("parent_work")
).order_by("-edition_rank") ).order_by("-edition_rank")
books = ( book_ids = cache.get_or_set(
models.Edition.viewer_aware_objects(request.user) f"author-books-{author.id}",
.filter(Q(authors=author) | Q(parent_work__authors=author)) lambda a: models.Edition.objects.filter(
Q(authors=a) | Q(parent_work__authors=a)
)
.annotate(default_id=Subquery(default_editions.values("id")[:1])) .annotate(default_id=Subquery(default_editions.values("id")[:1]))
.filter(default_id=F("id")) .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") .prefetch_related("authors")
).distinct() )
paginated = Paginator(books, PAGE_LENGTH) paginated = Paginator(books, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page")) page = paginated.get_page(request.GET.get("page"))

View file

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

View file

@ -46,9 +46,7 @@ class ReadingStatus(View):
return HttpResponseBadRequest() return HttpResponseBadRequest()
# invalidate related caches # invalidate related caches
cache.delete( cache.delete(f"active_shelf-{request.user.id}-{book_id}")
f"active_shelf-{request.user.id}-{book_id}",
)
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