Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2022-05-16 10:00:19 -07:00
commit b1f3253aa7
20 changed files with 183 additions and 27 deletions

View file

@ -16,7 +16,7 @@ If you'd like to join an instance, you can check out the [instances](https://joi
## Contributing ## Contributing
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions. See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions.
## About BookWyrm ## About BookWyrm
### What it is and isn't ### What it is and isn't
@ -76,4 +76,4 @@ Deployment
## Set up BookWyrm ## Set up BookWyrm
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html). The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html).

View file

@ -298,8 +298,9 @@ def add_status_on_create_command(sender, instance, created):
priority = HIGH priority = HIGH
# check if this is an old status, de-prioritize if so # check if this is an old status, de-prioritize if so
# (this will happen if federation is very slow, or, more expectedly, on csv import) # (this will happen if federation is very slow, or, more expectedly, on csv import)
one_day = 60 * 60 * 24 if instance.published_date < timezone.now() - timedelta(
if (instance.created_date - instance.published_date).seconds > one_day: days=1
) or instance.created_date < instance.published_date - timedelta(days=1):
priority = LOW priority = LOW
add_status_task.apply_async( add_status_task.apply_async(

View file

@ -8,6 +8,7 @@ from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField from .fields import RemoteIdField
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
remote_id = RemoteIdField(null=True, activitypub_field="id") remote_id = RemoteIdField(null=True, activitypub_field="id")
def get_remote_id(self): def get_remote_id(self):
"""generate a url that resolves to the local object""" """generate the url that resolves to the local object, without a slug"""
base_path = f"https://{DOMAIN}" base_path = f"https://{DOMAIN}"
if hasattr(self, "user"): if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}" base_path = f"{base_path}{self.user.local_path}"
model_name = type(self).__name__.lower() model_name = type(self).__name__.lower()
return f"{base_path}/{model_name}/{self.id}" return f"{base_path}/{model_name}/{self.id}"
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
@property @property
def local_path(self): def local_path(self):
"""how to link to this object in the local app""" """how to link to this object in the local app, with a slug"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "") local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
name = None
if hasattr(self, "name_field"):
name = getattr(self, self.name_field)
elif hasattr(self, "name"):
name = self.name
if name:
slug = slugify(name)
local = f"{local}/s/{slug}"
return local
def raise_visible_to_user(self, viewer): def raise_visible_to_user(self, viewer):
"""is a user authorized to view an object?""" """is a user authorized to view an object?"""

View file

@ -6,6 +6,7 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from . import fields from . import fields
@ -65,6 +66,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
identifier = self.identifier or self.get_identifier() identifier = self.identifier or self.get_identifier()
return f"{base_path}/books/{identifier}" return f"{base_path}/books/{identifier}"
@property
def local_path(self):
"""No slugs"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
def raise_not_deletable(self, viewer): def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf""" """don't let anyone delete a default shelf"""
super().raise_not_deletable(viewer) super().raise_not_deletable(viewer)

View file

@ -284,7 +284,7 @@
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %} {% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
{% url 'book' book.id as tab_url %} {% url 'book' book.id book.name|slugify as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
</li> </li>

View file

@ -15,7 +15,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs"> <nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul> <ul>
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li> <li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li>
<li class="is-active"> <li class="is-active">
<a href="#" aria-current="page"> <a href="#" aria-current="page">
{% trans "Edit links" %} {% trans "Edit links" %}

View file

@ -6,7 +6,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs"> <nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul> <ul>
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li> <li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li> <li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li>
<li class="is-active"> <li class="is-active">
<a href="#" aria-current="page"> <a href="#" aria-current="page">
{% trans "Curate" %} {% trans "Curate" %}

View file

@ -180,7 +180,7 @@
<h2 class="title is-5"> <h2 class="title is-5">
{% trans "Sort List" %} {% trans "Sort List" %}
</h2> </h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block"> <form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
<div class="field"> <div class="field">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label> <label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
<div class="select is-fullwidth"> <div class="select is-fullwidth">
@ -207,7 +207,7 @@
{% trans "Suggest Books" %} {% trans "Suggest Books" %}
{% endif %} {% endif %}
</h2> </h2>
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block"> <form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}"> <input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
@ -221,7 +221,7 @@
</div> </div>
</div> </div>
{% if query %} {% if query %}
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p> <p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p>
{% endif %} {% endif %}
</form> </form>
{% if not suggested_books %} {% if not suggested_books %}

View file

@ -1,6 +1,10 @@
""" testing activitystreams """ """ testing activitystreams """
from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@ -62,6 +66,39 @@ class ActivitystreamsSignals(TestCase):
self.assertEqual(args["args"][0], status.id) self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "high_priority") self.assertEqual(args["queue"], "high_priority")
def test_add_status_on_create_created_low_priority(self, *_):
"""a new statuses has entered"""
# created later than publication
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="public",
created_date=datetime(2022, 5, 16, tzinfo=timezone.utc),
published_date=datetime(2022, 5, 14, tzinfo=timezone.utc),
)
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
activitystreams.add_status_on_create_command(models.Status, status, False)
self.assertEqual(mock.call_count, 1)
args = mock.call_args[1]
self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "low_priority")
# published later than yesterday
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="public",
published_date=timezone.now() - timedelta(days=1),
)
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
activitystreams.add_status_on_create_command(models.Status, status, False)
self.assertEqual(mock.call_count, 1)
args = mock.call_args[1]
self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "low_priority")
def test_populate_streams_on_account_create_command(self, *_): def test_populate_streams_on_account_create_command(self, *_):
"""create streams for a user""" """create streams for a user"""
with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock: with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock:

View file

@ -101,6 +101,7 @@ class AbstractConnector(TestCase):
result = self.connector.get_or_create_book( result = self.connector.get_or_create_book(
f"https://{DOMAIN}/book/{self.book.id}" f"https://{DOMAIN}/book/{self.book.id}"
) )
self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book) self.assertEqual(result, self.book)

View file

@ -391,6 +391,9 @@ urlpatterns = [
re_path( re_path(
r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group" r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group"
), ),
re_path(
rf"^group/(?P<group_id>\d+){regex.SLUG}/?$", views.Group.as_view(), name="group"
),
re_path( re_path(
r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group" r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group"
), ),
@ -417,7 +420,10 @@ urlpatterns = [
re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"),
re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/(?P<list_id>\d+)(\.json)?/?$", views.List.as_view(), name="list"),
re_path(
rf"^list/(?P<list_id>\d+){regex.SLUG}/?$", views.List.as_view(), name="list"
),
re_path( re_path(
r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$", r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$",
views.ListItem.as_view(), views.ListItem.as_view(),
@ -487,6 +493,7 @@ urlpatterns = [
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock), re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
# statuses # statuses
re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"), re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}{regex.SLUG}/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"), re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"),
re_path( re_path(
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies" rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
@ -523,6 +530,7 @@ urlpatterns = [
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()), re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
# books # books
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"), re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
re_path( re_path(
rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$", rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$",
views.Book.as_view(), views.Book.as_view(),
@ -580,6 +588,11 @@ urlpatterns = [
re_path( re_path(
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author" r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
), ),
re_path(
rf"^author/(?P<author_id>\d+){regex.SLUG}/?$",
views.Author.as_view(),
name="author",
),
re_path( re_path(
r"^author/(?P<author_id>\d+)/edit/?$", r"^author/(?P<author_id>\d+)/edit/?$",
views.EditAuthor.as_view(), views.EditAuthor.as_view(),

View file

@ -6,5 +6,6 @@ STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?" USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b" STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b" FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
SLUG = r"/s/(?P<slug>[-_a-z0-9]*)"
# should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2; # should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2;
BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;" BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;"

View file

@ -11,20 +11,24 @@ 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.views.helpers import is_api_request from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable= no-self-use # pylint: disable= no-self-use
class Author(View): class Author(View):
"""this person wrote a book""" """this person wrote a book"""
def get(self, request, author_id): # pylint: disable=unused-argument
def get(self, request, author_id, slug=None):
"""landing page for an author""" """landing page for an author"""
author = get_object_or_404(models.Author, id=author_id) author = get_object_or_404(models.Author, id=author_id)
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(author.to_activity()) return ActivitypubResponse(author.to_activity())
if redirect_local_path := maybe_redirect_local_path(request, author):
return redirect_local_path
books = ( books = (
models.Work.objects.filter(editions__authors=author) models.Work.objects.filter(editions__authors=author)
.order_by("created_date") .order_by("created_date")

View file

@ -15,14 +15,14 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager, ConnectorException from bookwyrm.connectors import connector_manager, ConnectorException
from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable=no-self-use # pylint: disable=no-self-use
class Book(View): class Book(View):
"""a book! this is the stuff""" """a book! this is the stuff"""
def get(self, request, book_id, user_statuses=False, update_error=False): def get(self, request, book_id, **kwargs):
"""info about a book""" """info about a book"""
if is_api_request(request): if is_api_request(request):
book = get_object_or_404( book = get_object_or_404(
@ -30,7 +30,11 @@ class Book(View):
) )
return ActivitypubResponse(book.to_activity()) return ActivitypubResponse(book.to_activity())
user_statuses = user_statuses if request.user.is_authenticated else False user_statuses = (
kwargs.get("user_statuses", False)
if request.user.is_authenticated
else False
)
# it's safe to use this OR because edition and work and subclasses of the same # it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs # table, so they never have clashing IDs
@ -46,6 +50,11 @@ class Book(View):
if not book or not book.parent_work: if not book or not book.parent_work:
raise Http404() raise Http404()
if redirect_local_path := not user_statuses and maybe_redirect_local_path(
request, book
):
return redirect_local_path
# all reviews for all editions of the book # all reviews for all editions of the book
reviews = models.Review.privacy_filter(request.user).filter( reviews = models.Review.privacy_filter(request.user).filter(
book__parent_work__editions=book book__parent_work__editions=book
@ -80,7 +89,7 @@ class Book(View):
else None, else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"], "rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists, "lists": lists,
"update_error": update_error, "update_error": kwargs.get("update_error", False),
} }
if request.user.is_authenticated: if request.user.is_authenticated:

View file

@ -15,7 +15,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH, STREAMS from bookwyrm.settings import PAGE_LENGTH, STREAMS
from bookwyrm.suggested_users import suggested_users from bookwyrm.suggested_users import suggested_users
from .helpers import filter_stream_by_status_type, get_user_from_username from .helpers import filter_stream_by_status_type, get_user_from_username
from .helpers import is_api_request, is_bookwyrm_request from .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path
from .annual_summary import get_annual_summary_year from .annual_summary import get_annual_summary_year
@ -113,7 +113,8 @@ class DirectMessage(View):
class Status(View): class Status(View):
"""get posting""" """get posting"""
def get(self, request, username, status_id): # pylint: disable=unused-argument
def get(self, request, username, status_id, slug=None):
"""display a particular status (and replies, etc)""" """display a particular status (and replies, etc)"""
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
status = get_object_or_404( status = get_object_or_404(
@ -130,6 +131,9 @@ class Status(View):
status.to_activity(pure=not is_bookwyrm_request(request)) status.to_activity(pure=not is_bookwyrm_request(request))
) )
if redirect_local_path := maybe_redirect_local_path(request, status):
return redirect_local_path
visible_thread = ( visible_thread = (
models.Status.privacy_filter(request.user) models.Status.privacy_filter(request.user)
.filter(thread_id=status.thread_id) .filter(thread_id=status.thread_id)

View file

@ -14,17 +14,22 @@ from django.db.models.functions import Greatest
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.suggested_users import suggested_users from bookwyrm.suggested_users import suggested_users
from .helpers import get_user_from_username from .helpers import get_user_from_username, maybe_redirect_local_path
# pylint: disable=no-self-use # pylint: disable=no-self-use
class Group(View): class Group(View):
"""group page""" """group page"""
def get(self, request, group_id): # pylint: disable=unused-argument
def get(self, request, group_id, slug=None):
"""display a group""" """display a group"""
group = get_object_or_404(models.Group, id=group_id) group = get_object_or_404(models.Group, id=group_id)
group.raise_visible_to_user(request.user) group.raise_visible_to_user(request.user)
if redirect_local_path := maybe_redirect_local_path(request, group):
return redirect_local_path
lists = ( lists = (
models.List.privacy_filter(request.user) models.List.privacy_filter(request.user)
.filter(group=group) .filter(group=group)
@ -80,7 +85,8 @@ class Group(View):
class UserGroups(View): class UserGroups(View):
"""a user's groups page""" """a user's groups page"""
def get(self, request, username): # pylint: disable=unused-argument
def get(self, request, username, slug=None):
"""display a group""" """display a group"""
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
groups = ( groups = (

View file

@ -8,6 +8,7 @@ from dateutil.parser import ParserError
from requests import HTTPError from requests import HTTPError
from django.db.models import Q from django.db.models import Q
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.shortcuts import redirect
from django.http import Http404 from django.http import Http404
from django.utils import translation from django.utils import translation
@ -201,3 +202,21 @@ def filter_stream_by_status_type(activities, allowed_types=None):
) )
return activities return activities
def maybe_redirect_local_path(request, model):
"""
if the request had an invalid path, return a permanent redirect response to the
correct one, including a slug if any.
if path is valid, returns False.
"""
# don't redirect empty path for unit tests which currently have this
if request.path in ("/", model.local_path):
return False
new_path = model.local_path
if len(request.GET) > 0:
new_path = f"{model.local_path}?{request.GET.urlencode()}"
return redirect(new_path, permanent=True)

View file

@ -18,21 +18,27 @@ from django.views.decorators.http import require_POST
from bookwyrm import book_search, forms, models from bookwyrm import book_search, forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable=no-self-use # pylint: disable=no-self-use
class List(View): class List(View):
"""book list page""" """book list page"""
def get(self, request, list_id, add_failed=False, add_succeeded=False): def get(self, request, list_id, **kwargs):
"""display a book list""" """display a book list"""
add_failed = kwargs.get("add_failed", False)
add_succeeded = kwargs.get("add_succeeded", False)
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_visible_to_user(request.user) book_list.raise_visible_to_user(request.user)
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(book_list.to_activity(**request.GET)) return ActivitypubResponse(book_list.to_activity(**request.GET))
if r := maybe_redirect_local_path(request, book_list):
return r
query = request.GET.get("q") query = request.GET.get("q")
suggestions = None suggestions = None

View file

@ -24,6 +24,8 @@ class Search(View):
def get(self, request): def get(self, request):
"""that search bar up top""" """that search bar up top"""
query = request.GET.get("q") query = request.GET.get("q")
# check if query is isbn
query = isbn_check(query)
min_confidence = request.GET.get("min_confidence", 0) min_confidence = request.GET.get("min_confidence", 0)
search_type = request.GET.get("type") search_type = request.GET.get("type")
search_remote = ( search_remote = (
@ -123,3 +125,35 @@ def list_search(query, viewer, *_):
) )
.order_by("-similarity") .order_by("-similarity")
), None ), None
def isbn_check(query):
"""isbn10 or isbn13 check, if so remove separators"""
if query:
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
if len(su_num) == 13 and su_num.isdecimal():
# Multiply every other digit by 3
# Add these numbers and the other digits
product = sum(int(ch) for ch in su_num[::2]) + sum(
int(ch) * 3 for ch in su_num[1::2]
)
if product % 10 == 0:
return su_num
elif (
len(su_num) == 10
and su_num[:-1].isdecimal()
and (su_num[-1].isdecimal() or su_num[-1].lower() == "x")
):
product = 0
# Iterate through code_string
for i in range(9):
# for each character, multiply by a different decreasing number: 10 - x
product = product + int(su_num[i]) * (10 - i)
# Handle last character
if su_num[9].lower() == "x":
product += 10
else:
product += int(su_num[9])
if product % 11 == 0:
return su_num
return query

View file

@ -3,6 +3,7 @@ version: '3'
services: services:
nginx: nginx:
image: nginx:latest image: nginx:latest
restart: unless-stopped
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443