1
0
Fork 0

Merge branch 'main' into move-fix

This commit is contained in:
Bart Schuurmans 2024-04-24 14:56:32 +02:00 committed by GitHub
commit 839ab2fafd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
315 changed files with 19376 additions and 4583 deletions

View file

@ -5,6 +5,7 @@ from .admin.announcements import EditAnnouncement, delete_announcement
from .admin.automod import AutoMod, automod_delete, run_automod
from .admin.automod import schedule_automod_task, unschedule_automod_task
from .admin.celery_status import CeleryStatus, celery_ping
from .admin.schedule import ScheduledTasks
from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist

View file

@ -6,7 +6,7 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from django_celery_beat.models import PeriodicTask
from django_celery_beat.models import PeriodicTask, IntervalSchedule
from bookwyrm import forms, models
@ -54,7 +54,7 @@ def schedule_automod_task(request):
return TemplateResponse(request, "settings/automod/rules.html", data)
with transaction.atomic():
schedule = form.save(request)
schedule, _ = IntervalSchedule.objects.get_or_create(**form.cleaned_data)
PeriodicTask.objects.get_or_create(
interval=schedule,
name="automod-task",

View file

@ -6,16 +6,18 @@ from dateutil.parser import parse
from packaging import version
from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.db.models import Q
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from django_celery_beat.models import PeriodicTask, IntervalSchedule
from csp.decorators import csp_update
from bookwyrm import models, settings
from bookwyrm.connectors.abstract_connector import get_data
from bookwyrm import forms, models, settings
from bookwyrm.utils import regex
@ -59,21 +61,36 @@ class Dashboard(View):
== site._meta.get_field("privacy_policy").get_default()
)
# check version
if site.available_version and version.parse(
site.available_version
) > version.parse(settings.VERSION):
data["current_version"] = settings.VERSION
data["available_version"] = site.available_version
try:
release = get_data(settings.RELEASE_API, timeout=3)
available_version = release.get("tag_name", None)
if available_version and version.parse(available_version) > version.parse(
settings.VERSION
):
data["current_version"] = settings.VERSION
data["available_version"] = available_version
except: # pylint: disable= bare-except
pass
if not PeriodicTask.objects.filter(name="check-for-updates").exists():
data["schedule_form"] = forms.IntervalScheduleForm(
{"every": 1, "period": "days"}
)
return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
def post(self, request):
"""Create a schedule task to check for updates"""
schedule_form = forms.IntervalScheduleForm(request.POST)
if not schedule_form.is_valid():
raise schedule_form.ValidationError(schedule_form.errors)
with transaction.atomic():
schedule, _ = IntervalSchedule.objects.get_or_create(
**schedule_form.cleaned_data
)
PeriodicTask.objects.get_or_create(
interval=schedule,
name="check-for-updates",
task="bookwyrm.models.site.check_for_updates_task",
)
return redirect("settings-dashboard")
def get_charts_and_stats(request):
"""Defines the dashboard charts"""

View file

@ -9,7 +9,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.views.helpers import redirect_to_referer
from bookwyrm.settings import PAGE_LENGTH, USE_S3
from bookwyrm.settings import PAGE_LENGTH, USE_AZURE
# pylint: disable=no-self-use
@ -59,7 +59,7 @@ class ImportList(View):
"import_size_limit": site_settings.import_size_limit,
"import_limit_reset": site_settings.import_limit_reset,
"user_import_time_limit": site_settings.user_import_time_limit,
"use_s3": USE_S3,
"use_azure": USE_AZURE,
}
return TemplateResponse(request, "settings/imports/imports.html", data)

View file

@ -0,0 +1,31 @@
""" Scheduled celery tasks """
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django_celery_beat.models import PeriodicTask, IntervalSchedule
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
# pylint: disable=no-self-use
class ScheduledTasks(View):
"""Manage automated flagging"""
def get(self, request):
"""view schedules"""
data = {}
data["tasks"] = PeriodicTask.objects.all()
data["schedules"] = IntervalSchedule.objects.all()
return TemplateResponse(request, "settings/schedules.html", data)
# pylint: disable=unused-argument
def post(self, request, task_id):
"""un-schedule a task"""
task = PeriodicTask.objects.get(id=task_id)
task.delete()
return redirect("settings-schedules")

View file

@ -1,4 +1,5 @@
""" the good people stuff! the authors! """
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect
@ -11,7 +12,11 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
from bookwyrm.views.helpers import (
is_api_request,
get_mergeable_object_or_404,
maybe_redirect_local_path,
)
# pylint: disable= no-self-use
@ -21,7 +26,7 @@ class Author(View):
# pylint: disable=unused-argument
def get(self, request, author_id, slug=None):
"""landing page for an author"""
author = get_object_or_404(models.Author, id=author_id)
author = get_mergeable_object_or_404(models.Author, id=author_id)
if is_api_request(request):
return ActivitypubResponse(author.to_activity())
@ -56,13 +61,13 @@ class EditAuthor(View):
def get(self, request, author_id):
"""info about a book"""
author = get_object_or_404(models.Author, id=author_id)
author = get_mergeable_object_or_404(models.Author, id=author_id)
data = {"author": author, "form": forms.AuthorForm(instance=author)}
return TemplateResponse(request, "author/edit_author.html", data)
def post(self, request, author_id):
"""edit a author cool"""
author = get_object_or_404(models.Author, id=author_id)
author = get_mergeable_object_or_404(models.Author, id=author_id)
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
if not form.is_valid():
@ -82,7 +87,7 @@ def update_author_from_remote(request, author_id, connector_identifier):
connector = connector_manager.load_connector(
get_object_or_404(models.Connector, identifier=connector_identifier)
)
author = get_object_or_404(models.Author, id=author_id)
author = get_mergeable_object_or_404(models.Author, id=author_id)
connector.update_author_from_remote(author)

View file

@ -1,4 +1,5 @@
""" the good stuff! the books! """
from uuid import uuid4
from django.contrib.auth.decorators import login_required, permission_required
@ -15,7 +16,11 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager, ConnectorException
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
from bookwyrm.views.helpers import (
is_api_request,
maybe_redirect_local_path,
get_mergeable_object_or_404,
)
# pylint: disable=no-self-use
@ -40,7 +45,11 @@ class Book(View):
# table, so they never have clashing IDs
book = (
models.Edition.viewer_aware_objects(request.user)
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
.filter(
Q(id=book_id)
| Q(parent_work__id=book_id)
| Q(absorbed__deleted_id=book_id)
)
.order_by("-edition_rank")
.select_related("parent_work")
.prefetch_related("authors", "file_links")
@ -82,11 +91,13 @@ class Book(View):
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
"ratings": reviews.filter(
Q(content__isnull=True) | Q(content="")
).select_related("user")
if not user_statuses
else None,
"ratings": (
reviews.filter(Q(content__isnull=True) | Q(content="")).select_related(
"user"
)
if not user_statuses
else None
),
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists,
"update_error": kwargs.get("update_error", False),
@ -130,7 +141,7 @@ class Book(View):
@require_POST
def upload_cover(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
book.last_edited_by = request.user
url = request.POST.get("cover-url")
@ -168,7 +179,7 @@ def set_cover_from_url(url):
@permission_required("bookwyrm.edit_book", raise_exception=True)
def add_description(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
description = request.POST.get("description")
@ -199,7 +210,9 @@ def update_book_from_remote(request, book_id, connector_identifier):
connector = connector_manager.load_connector(
get_object_or_404(models.Connector, identifier=connector_identifier)
)
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
book = get_mergeable_object_or_404(
models.Book.objects.select_subclasses(), id=book_id
)
try:
connector.update_book_from_remote(book)

View file

@ -1,4 +1,5 @@
""" the good stuff! the books! """
from re import sub, findall
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
@ -18,9 +19,10 @@ from bookwyrm.utils.isni import (
build_author_from_isni,
augment_author_metadata,
)
from bookwyrm.views.helpers import get_edition
from bookwyrm.views.helpers import get_edition, get_mergeable_object_or_404
from .books import set_cover_from_url
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
@ -42,7 +44,7 @@ class EditBook(View):
def post(self, request, book_id):
"""edit a book cool"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
form = forms.EditionForm(request.POST, request.FILES, instance=book)
@ -130,7 +132,7 @@ class CreateBook(View):
with transaction.atomic():
book = form.save(request)
parent_work = get_object_or_404(models.Work, id=parent_work_id)
parent_work = get_mergeable_object_or_404(models.Work, id=parent_work_id)
book.parent_work = parent_work
if authors:
@ -295,7 +297,7 @@ class ConfirmEditBook(View):
if not book.parent_work:
work_match = request.POST.get("parent_work")
if work_match and work_match != "0":
work = get_object_or_404(models.Work, id=work_match)
work = get_mergeable_object_or_404(models.Work, id=work_match)
else:
work = models.Work.objects.create(title=form.cleaned_data["title"])
work.authors.set(book.authors.all())

View file

@ -1,12 +1,14 @@
""" the good stuff! the books! """
from functools import reduce
import operator
from django.contrib.auth.decorators import login_required
from django.core.cache import cache as django_cache
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views import View
from django.views.decorators.http import require_POST
@ -14,7 +16,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request
from bookwyrm.views.helpers import is_api_request, get_mergeable_object_or_404
# pylint: disable=no-self-use
@ -23,7 +25,7 @@ class Editions(View):
def get(self, request, book_id):
"""list of editions of a book"""
work = get_object_or_404(models.Work, id=book_id)
work = get_mergeable_object_or_404(models.Work, id=book_id)
if is_api_request(request):
return ActivitypubResponse(work.to_edition_list(**request.GET))
@ -82,7 +84,7 @@ class Editions(View):
def switch_edition(request):
"""switch your copy of a book to a different edition"""
edition_id = request.POST.get("edition")
new_edition = get_object_or_404(models.Edition, id=edition_id)
new_edition = get_mergeable_object_or_404(models.Edition, id=edition_id)
shelfbooks = models.ShelfBook.objects.filter(
book__parent_work=new_edition.parent_work, shelf__user=request.user
)
@ -93,6 +95,7 @@ def switch_edition(request):
user=shelfbook.user,
shelf=shelfbook.shelf,
book=new_edition,
shelved_date=shelfbook.shelved_date,
)
shelfbook.delete()
@ -103,4 +106,20 @@ def switch_edition(request):
readthrough.book = new_edition
readthrough.save()
django_cache.delete_many(
[
f"active_shelf-{request.user.id}-{book_id}"
for book_id in new_edition.parent_work.editions.values_list("id", flat=True)
]
)
reviews = models.Review.objects.filter(
book__parent_work=new_edition.parent_work, user=request.user
)
for review in reviews.all():
# because ratings are a subclass of reviews,
# this will pick up both ratings and reviews
review.book = new_edition
review.save()
return redirect(f"/book/{new_edition.id}")

View file

@ -1,4 +1,5 @@
""" the good stuff! the books! """
from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
@ -8,6 +9,7 @@ from django.utils.decorators import method_decorator
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.views.helpers import get_mergeable_object_or_404
# pylint: disable=no-self-use
@ -20,7 +22,7 @@ class BookFileLinks(View):
def get(self, request, book_id):
"""view links"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
annotated_links = get_annotated_links(book)
data = {"book": book, "links": annotated_links}
@ -36,7 +38,7 @@ class BookFileLinks(View):
# this form shouldn't ever really get here, since it's just a dropdown
# get the data again rather than redirecting
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
annotated_links = get_annotated_links(book, form=form)
data = {"book": book, "links": annotated_links}
@ -75,7 +77,7 @@ class AddFileLink(View):
def get(self, request, book_id):
"""Create link form"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
data = {
"file_link_form": forms.FileLinkForm(),
"book": book,
@ -85,7 +87,9 @@ class AddFileLink(View):
@transaction.atomic
def post(self, request, book_id, link_id=None):
"""Add a link to a copy of the book you can read"""
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
book = get_mergeable_object_or_404(
models.Book.objects.select_subclasses(), id=book_id
)
link = get_object_or_404(models.FileLink, id=link_id) if link_id else None
form = forms.FileLinkForm(request.POST, instance=link)
if not form.is_valid():

View file

@ -1,10 +1,10 @@
""" books belonging to the same series """
from sys import float_info
from django.views import View
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from bookwyrm.views.helpers import is_api_request
from bookwyrm.views.helpers import is_api_request, get_mergeable_object_or_404
from bookwyrm import models
@ -27,7 +27,7 @@ class BookSeriesBy(View):
if is_api_request(request):
pass
author = get_object_or_404(models.Author, id=author_id)
author = get_mergeable_object_or_404(models.Author, id=author_id)
results = models.Edition.objects.filter(authors=author, series=series_name)
@ -56,9 +56,11 @@ class BookSeriesBy(View):
sorted(numbered_books, key=sort_by_series)
+ sorted(
dated_books,
key=lambda book: book.first_published_date
if book.first_published_date
else book.published_date,
key=lambda book: (
book.first_published_date
if book.first_published_date
else book.published_date
),
)
+ sorted(
unsortable_books,

View file

@ -1,4 +1,5 @@
""" non-interactive pages """
from datetime import date
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
@ -52,6 +53,19 @@ class Feed(View):
suggestions = suggested_users.get_suggestions(request.user)
cutoff = (
date(get_annual_summary_year(), 12, 31)
if get_annual_summary_year()
else None
)
readthroughs = (
models.ReadThrough.objects.filter(
user=request.user, finish_date__lte=cutoff
)
if get_annual_summary_year()
else []
)
data = {
**feed_page_data(request.user),
**{
@ -66,6 +80,7 @@ class Feed(View):
"path": f"/{tab['key']}",
"annual_summary_year": get_annual_summary_year(),
"has_tour": True,
"has_summary_read_throughs": len(readthroughs),
},
}
return TemplateResponse(request, "feed/feed.html", data)
@ -185,19 +200,15 @@ class Status(View):
params=[status.id, visible_thread, visible_thread],
)
preview = None
if hasattr(status, "book"):
preview = status.book.preview_image
elif status.mention_books.exists():
preview = status.mention_books.first().preview_image
data = {
**feed_page_data(request.user),
**{
"status": status,
"children": children,
"ancestors": ancestors,
"preview": preview,
"title": status.page_title,
"description": status.page_description,
"page_image": status.page_image,
},
}
return TemplateResponse(request, "feed/status.html", data)

View file

@ -1,4 +1,5 @@
""" Helping new users figure out the lay of the land """
import re
from django.contrib.auth.decorators import login_required
@ -13,6 +14,7 @@ from django.views import View
from bookwyrm import book_search, forms, models
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
from bookwyrm.suggested_users import suggested_users
from bookwyrm.views.helpers import get_mergeable_object_or_404
from .preferences.edit_user import save_user_form
@ -80,8 +82,8 @@ class GetStartedBooks(View):
for k, v in request.POST.items()
if re.match(r"\d+", k) and re.match(r"\d+", v)
]
for (book_id, shelf_id) in shelve_actions:
book = get_object_or_404(models.Edition, id=book_id)
for book_id, shelf_id in shelve_actions:
book = get_mergeable_object_or_404(models.Edition, id=book_id)
shelf = get_object_or_404(models.Shelf, id=shelf_id)
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)

View file

@ -1,4 +1,5 @@
""" helper functions used in various views """
import re
from datetime import datetime, timedelta
import dateutil.parser
@ -8,7 +9,7 @@ from dateutil.parser import ParserError
from requests import HTTPError
from django.db.models import Q
from django.conf import settings as django_settings
from django.shortcuts import redirect
from django.shortcuts import redirect, _get_queryset
from django.http import Http404
from django.utils import translation
@ -237,3 +238,19 @@ def redirect_to_referer(request, *args, **kwargs):
# if not, use the args passed you'd normally pass to redirect()
return redirect(*args or "/", **kwargs)
# pylint: disable=redefined-builtin,invalid-name
def get_mergeable_object_or_404(klass, id):
"""variant of get_object_or_404 that also redirects if id has been merged
into another object"""
queryset = _get_queryset(klass)
try:
return queryset.get(pk=id)
except queryset.model.DoesNotExist:
try:
return queryset.get(absorbed__deleted_id=id)
except queryset.model.DoesNotExist:
pass
raise Http404(f"No {queryset.model} with ID {id} exists")

View file

@ -6,16 +6,19 @@ import io
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseServerError, Http404
from django.template.response import TemplateResponse
from django.utils import timezone
from django.views import View
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.shortcuts import redirect
from bookwyrm import models
from storages.backends.s3boto3 import S3Boto3Storage
from bookwyrm import models, storage_backends
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm import settings
# pylint: disable=no-self-use,too-many-locals
@ -144,25 +147,53 @@ class Export(View):
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
class ExportUser(View):
"""Let users export user data to import into another Bookwyrm instance"""
"""
Let users request and download an archive of user data to import into
another Bookwyrm instance.
"""
user_jobs = None
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.user_jobs = BookwyrmExportJob.objects.filter(user=request.user).order_by(
"-created_date"
)
def new_export_blocked_until(self):
"""whether the user is allowed to request a new export"""
last_job = self.user_jobs.first()
if not last_job:
return None
site = models.SiteSettings.objects.get()
blocked_until = last_job.created_date + timedelta(
hours=site.user_import_time_limit
)
return blocked_until if blocked_until > timezone.now() else None
def get(self, request):
"""Request tar file"""
jobs = BookwyrmExportJob.objects.filter(user=request.user).order_by(
"-created_date"
)
site = models.SiteSettings.objects.get()
hours = site.user_import_time_limit
allowed = (
jobs.first().created_date < timezone.now() - timedelta(hours=hours)
if jobs.first()
else True
)
next_available = (
jobs.first().created_date + timedelta(hours=hours) if not allowed else False
)
paginated = Paginator(jobs, PAGE_LENGTH)
exports = []
for job in self.user_jobs:
export = {"job": job}
if job.export_data:
try:
export["size"] = job.export_data.size
export["url"] = reverse("prefs-export-file", args=[job.task_id])
except FileNotFoundError:
# file no longer exists locally
export["unavailable"] = True
except Exception: # pylint: disable=broad-except
# file no longer exists on storage backend
export["unavailable"] = True
exports.append(export)
next_available = self.new_export_blocked_until()
paginated = Paginator(exports, settings.PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data = {
"jobs": page,
@ -175,7 +206,9 @@ class ExportUser(View):
return TemplateResponse(request, "preferences/export-user.html", data)
def post(self, request):
"""Download the json file of a user's data"""
"""Trigger processing of a new user export file"""
if self.new_export_blocked_until() is not None:
return HttpResponse(status=429) # Too Many Requests
job = BookwyrmExportJob.objects.create(user=request.user)
job.start_job()
@ -187,13 +220,35 @@ class ExportUser(View):
class ExportArchive(View):
"""Serve the archive file"""
# TODO: how do we serve s3 files?
def get(self, request, archive_id):
"""download user export file"""
export = BookwyrmExportJob.objects.get(task_id=archive_id, user=request.user)
return HttpResponse(
export.export_data,
content_type="application/gzip",
headers={
"Content-Disposition": 'attachment; filename="bookwyrm-account-export.tar.gz"' # pylint: disable=line-too-long
},
)
if isinstance(export.export_data.storage, storage_backends.ExportsS3Storage):
# make custom_domain None so we can sign the url
# see https://github.com/jschneier/django-storages/issues/944
storage = S3Boto3Storage(querystring_auth=True, custom_domain=None)
try:
url = S3Boto3Storage.url(
storage,
f"/exports/{export.task_id}.tar.gz",
expire=settings.S3_SIGNED_URL_EXPIRY,
)
except Exception:
raise Http404()
return redirect(url)
if isinstance(export.export_data.storage, storage_backends.ExportsFileStorage):
try:
return HttpResponse(
export.export_data,
content_type="application/gzip",
headers={
"Content-Disposition": 'attachment; filename="bookwyrm-account-export.tar.gz"' # pylint: disable=line-too-long
},
)
except FileNotFoundError:
raise Http404()
return HttpResponseServerError()

View file

@ -1,4 +1,5 @@
""" the good stuff! the books! """
import logging
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
@ -11,6 +12,7 @@ from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.views.helpers import get_mergeable_object_or_404
from bookwyrm.views.shelf.shelf_actions import unshelve
from .status import CreateStatus
from .helpers import get_edition, handle_reading_status, is_api_request
@ -130,7 +132,7 @@ class ReadThrough(View):
def get(self, request, book_id, readthrough_id=None):
"""standalone form in case of errors"""
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
form = forms.ReadThroughForm()
data = {"form": form, "book": book}
if readthrough_id:
@ -152,7 +154,7 @@ class ReadThrough(View):
)
form = forms.ReadThroughForm(request.POST)
if not form.is_valid():
book = get_object_or_404(models.Edition, id=book_id)
book = get_mergeable_object_or_404(models.Edition, id=book_id)
data = {"form": form, "book": book}
if request.POST.get("id"):
data["readthrough"] = get_object_or_404(

View file

@ -1,8 +1,10 @@
""" search views"""
import re
from django.contrib.postgres.search import TrigramSimilarity
from django.contrib.postgres.search import TrigramSimilarity, SearchRank, SearchQuery
from django.core.paginator import Paginator
from django.db.models import F
from django.db.models.functions import Greatest
from django.http import JsonResponse
from django.template.response import TemplateResponse
@ -39,6 +41,7 @@ class Search(View):
endpoints = {
"book": book_search,
"author": author_search,
"user": user_search,
"list": list_search,
}
@ -90,6 +93,33 @@ def book_search(request):
return TemplateResponse(request, "search/book.html", data)
def author_search(request):
"""search for an author"""
query = request.GET.get("q").strip()
search_query = SearchQuery(query, config="simple")
min_confidence = 0
results = (
models.Author.objects.filter(search_vector=search_query)
.annotate(rank=SearchRank(F("search_vector"), search_query))
.filter(rank__gt=min_confidence)
.order_by("-rank")
)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data = {
"type": "author",
"query": query,
"results": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
}
return TemplateResponse(request, "search/author.html", data)
def user_search(request):
"""user search: search for a user"""
viewer = request.user

View file

@ -1,11 +1,12 @@
""" shelf views """
from django.db import IntegrityError, transaction
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.views.helpers import redirect_to_referer
from bookwyrm.views.helpers import redirect_to_referer, get_mergeable_object_or_404
@login_required
@ -36,7 +37,7 @@ def delete_shelf(request, shelf_id):
@transaction.atomic
def shelve(request):
"""put a book on a user's shelf"""
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
book = get_mergeable_object_or_404(models.Edition, id=request.POST.get("book"))
desired_shelf = get_object_or_404(
request.user.shelf_set, identifier=request.POST.get("shelf")
)
@ -97,7 +98,7 @@ def shelve(request):
def unshelve(request, book_id=False):
"""remove a book from a user's shelf"""
identity = book_id if book_id else request.POST.get("book")
book = get_object_or_404(models.Edition, id=identity)
book = get_mergeable_object_or_404(models.Edition, id=identity)
shelf_book = get_object_or_404(
models.ShelfBook, book=book, shelf__id=request.POST["shelf"]
)

View file

@ -1,4 +1,5 @@
""" what are we here for if not for posting """
import re
import logging
@ -19,6 +20,7 @@ from markdown import markdown
from bookwyrm import forms, models
from bookwyrm.models.report import DELETE_ITEM
from bookwyrm.utils import regex, sanitizer
from bookwyrm.views.helpers import get_mergeable_object_or_404
from .helpers import handle_remote_webfinger, is_api_request
from .helpers import load_date_in_user_tz_as_utc, redirect_to_referer
@ -52,7 +54,7 @@ class CreateStatus(View):
def get(self, request, status_type): # pylint: disable=unused-argument
"""compose view (...not used?)"""
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
book = get_mergeable_object_or_404(models.Edition, id=request.GET.get("book"))
data = {"book": book}
return TemplateResponse(request, "compose.html", data)
@ -98,7 +100,7 @@ class CreateStatus(View):
# inspect the text for user tags
content = status.content
mentions = find_mentions(request.user, content)
for (_, mention_user) in mentions.items():
for _, mention_user in mentions.items():
# add them to status mentions fk
status.mention_users.add(mention_user)
content = format_mentions(content, mentions)
@ -109,7 +111,7 @@ class CreateStatus(View):
# inspect the text for hashtags
hashtags = find_or_create_hashtags(content)
for (_, mention_hashtag) in hashtags.items():
for _, mention_hashtag in hashtags.items():
# add them to status mentions fk
status.mention_hashtags.add(mention_hashtag)
content = format_hashtags(content, hashtags)
@ -140,7 +142,7 @@ class CreateStatus(View):
def format_mentions(content, mentions):
"""Detect @mentions and make them links"""
for (mention_text, mention_user) in mentions.items():
for mention_text, mention_user in mentions.items():
# turn the mention into a link
content = re.sub(
rf"(?<!/)\B{mention_text}\b(?!@)",
@ -152,7 +154,7 @@ def format_mentions(content, mentions):
def format_hashtags(content, hashtags):
"""Detect #hashtags and make them links"""
for (mention_text, mention_hashtag) in hashtags.items():
for mention_text, mention_hashtag in hashtags.items():
# turn the mention into a link
content = re.sub(
rf"(?<!/)\B{mention_text}\b(?!@)",