moviewyrm/bookwyrm/views/books.py

393 lines
14 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" the good stuff! the books! """
2021-03-18 17:03:53 +00:00
from uuid import uuid4
from dateutil.parser import parse as dateparse
2021-01-13 17:54:35 +00:00
from django.contrib.auth.decorators import login_required, permission_required
2021-03-04 21:48:50 +00:00
from django.contrib.postgres.search import SearchRank, SearchVector
2021-03-18 17:03:53 +00:00
from django.core.files.base import ContentFile
2021-03-04 21:48:50 +00:00
from django.core.paginator import Paginator
2021-01-13 17:54:35 +00:00
from django.db import transaction
from django.db.models import Avg, Q
2021-03-14 02:09:09 +00:00
from django.http import HttpResponseBadRequest, HttpResponseNotFound
2021-01-13 17:54:35 +00:00
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
2021-01-13 17:54:35 +00:00
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
2021-03-18 17:03:53 +00:00
from bookwyrm.connectors.abstract_connector import get_image
2021-01-13 17:54:35 +00:00
from bookwyrm.settings import PAGE_LENGTH
2021-03-23 02:17:46 +00:00
from .helpers import is_api_request, get_edition, privacy_filter
2021-01-13 17:54:35 +00:00
# pylint: disable= no-self-use
class Book(View):
2021-04-26 16:15:42 +00:00
"""a book! this is the stuff"""
2021-03-08 16:49:10 +00:00
2021-04-23 20:32:58 +00:00
def get(self, request, book_id, user_statuses=False):
2021-04-26 16:15:42 +00:00
"""info about a book"""
2021-05-18 18:09:19 +00:00
user_statuses = user_statuses if request.user.is_authenticated else False
2021-01-13 17:54:35 +00:00
try:
book = models.Book.objects.select_subclasses().get(id=book_id)
except models.Book.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(book.to_activity())
if isinstance(book, models.Work):
book = book.default_edition
2021-04-23 20:32:58 +00:00
if not book or not book.parent_work:
2021-01-13 17:54:35 +00:00
return HttpResponseNotFound()
work = book.parent_work
# all reviews for the book
2021-04-23 20:32:58 +00:00
reviews = privacy_filter(
2021-04-23 20:46:22 +00:00
request.user, models.Review.objects.filter(book__in=work.editions.all())
2021-04-23 20:32:58 +00:00
)
2021-01-13 17:54:35 +00:00
# the reviews to show
2021-05-18 18:09:19 +00:00
if user_statuses:
2021-04-23 20:46:22 +00:00
if user_statuses == "review":
2021-05-18 18:17:59 +00:00
queryset = book.review_set.select_subclasses()
2021-04-23 20:46:22 +00:00
elif user_statuses == "comment":
2021-04-23 20:32:58 +00:00
queryset = book.comment_set
else:
queryset = book.quotation_set
2021-04-23 20:46:22 +00:00
queryset = queryset.filter(user=request.user)
2021-04-23 20:32:58 +00:00
else:
2021-04-23 20:46:22 +00:00
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
2021-05-23 03:14:57 +00:00
queryset = queryset.select_related("user")
2021-04-23 20:46:22 +00:00
paginated = Paginator(queryset, PAGE_LENGTH)
2021-04-23 20:32:58 +00:00
data = {
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
2021-05-23 03:14:57 +00:00
"ratings": reviews.filter(
Q(content__isnull=True) | Q(content="")
).select_related("user")
2021-05-18 18:09:19 +00:00
if not user_statuses
else None,
2021-04-23 20:32:58 +00:00
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": privacy_filter(
request.user, book.list_set.filter(listitem__approved=True)
),
}
2021-01-13 17:54:35 +00:00
if request.user.is_authenticated:
readthroughs = models.ReadThrough.objects.filter(
user=request.user,
book=book,
2021-03-08 16:49:10 +00:00
).order_by("start_date")
2021-01-13 17:54:35 +00:00
for readthrough in readthroughs:
2021-03-08 16:49:10 +00:00
readthrough.progress_updates = (
readthrough.progressupdate_set.all().order_by("-updated_date")
)
2021-04-23 20:32:58 +00:00
data["readthroughs"] = readthroughs
2021-05-23 03:14:57 +00:00
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
2021-04-23 20:32:58 +00:00
user=request.user, book=book
2021-05-23 03:14:57 +00:00
).select_related("shelf")
2021-01-13 17:54:35 +00:00
2021-04-23 20:32:58 +00:00
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
2021-01-13 17:54:35 +00:00
~Q(book=book),
user=request.user,
2021-01-13 17:54:35 +00:00
book__parent_work=book.parent_work,
2021-05-23 03:14:57 +00:00
).select_related("shelf", "book")
2021-01-13 17:54:35 +00:00
2021-04-23 20:32:58 +00:00
data["user_statuses"] = {
"review_count": book.review_set.filter(user=request.user).count(),
"comment_count": book.comment_set.filter(user=request.user).count(),
"quotation_count": book.quotation_set.filter(user=request.user).count(),
}
2021-03-18 16:37:16 +00:00
return TemplateResponse(request, "book/book.html", data)
2021-01-13 17:54:35 +00:00
2021-03-08 16:49:10 +00:00
@method_decorator(login_required, name="dispatch")
2021-01-13 17:54:35 +00:00
@method_decorator(
2021-03-08 16:49:10 +00:00
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
)
2021-01-13 17:54:35 +00:00
class EditBook(View):
2021-04-26 16:15:42 +00:00
"""edit a book"""
2021-03-08 18:10:30 +00:00
def get(self, request, book_id=None):
2021-04-26 16:15:42 +00:00
"""info about a book"""
book = None
if book_id:
book = get_edition(book_id)
if not book.description:
book.description = book.parent_work.description
2021-03-08 18:10:30 +00:00
data = {"book": book, "form": forms.EditionForm(instance=book)}
2021-03-18 16:37:16 +00:00
return TemplateResponse(request, "book/edit_book.html", data)
2021-01-13 17:54:35 +00:00
def post(self, request, book_id=None):
2021-04-26 16:15:42 +00:00
"""edit a book cool"""
2021-03-05 01:09:49 +00:00
# returns None if no match is found
book = models.Edition.objects.filter(id=book_id).first()
2021-01-13 17:54:35 +00:00
form = forms.EditionForm(request.POST, request.FILES, instance=book)
2021-03-08 18:10:30 +00:00
data = {"book": book, "form": form}
2021-01-13 17:54:35 +00:00
if not form.is_valid():
2021-03-18 16:37:16 +00:00
return TemplateResponse(request, "book/edit_book.html", data)
2021-03-08 18:10:30 +00:00
add_author = request.POST.get("add_author")
2021-03-05 01:09:49 +00:00
# we're adding an author through a free text field
if add_author:
2021-03-08 18:10:30 +00:00
data["add_author"] = add_author
2021-03-12 00:40:35 +00:00
data["author_matches"] = []
for author in add_author.split(","):
2021-03-12 17:46:28 +00:00
if not author:
continue
2021-03-12 00:33:49 +00:00
# check for existing authors
vector = SearchVector("name", weight="A") + SearchVector(
"aliases", weight="B"
)
2021-03-04 21:48:50 +00:00
2021-03-12 00:40:35 +00:00
data["author_matches"].append(
{
"name": author.strip(),
"matches": (
models.Author.objects.annotate(search=vector)
2021-03-14 02:09:09 +00:00
.annotate(rank=SearchRank(vector, author))
2021-03-12 00:40:35 +00:00
.filter(rank__gt=0.4)
.order_by("-rank")[:5]
),
}
)
2021-03-04 21:48:50 +00:00
2021-03-05 01:09:49 +00:00
# we're creating a new book
if not book:
2021-03-04 21:48:50 +00:00
# check if this is an edition of an existing work
author_text = book.author_text if book else add_author
2021-03-08 18:10:30 +00:00
data["book_matches"] = connector_manager.local_search(
"%s %s" % (form.cleaned_data.get("title"), author_text),
2021-03-04 21:48:50 +00:00
min_confidence=0.5,
2021-03-08 18:10:30 +00:00
raw=True,
2021-03-04 21:48:50 +00:00
)[:5]
2021-03-05 01:09:49 +00:00
# either of the above cases requires additional confirmation
if add_author or not book:
# creting a book or adding an author to a book needs another step
2021-03-08 18:10:30 +00:00
data["confirm_mode"] = True
2021-03-12 00:33:49 +00:00
# this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors")
# make sure the dates are passed in as datetime, they're currently a string
# QueryDicts are immutable, we need to copy
formcopy = data["form"].data.copy()
try:
formcopy["first_published_date"] = dateparse(
formcopy["first_published_date"]
)
except (MultiValueDictKeyError, ValueError):
pass
try:
formcopy["published_date"] = dateparse(formcopy["published_date"])
except (MultiValueDictKeyError, ValueError):
pass
data["form"].data = formcopy
2021-03-18 16:37:16 +00:00
return TemplateResponse(request, "book/edit_book.html", data)
2021-01-13 17:54:35 +00:00
2021-03-08 18:10:30 +00:00
remove_authors = request.POST.getlist("remove_authors")
2021-03-07 22:19:22 +00:00
for author_id in remove_authors:
book.authors.remove(author_id)
book = form.save(commit=False)
url = request.POST.get("cover-url")
if url:
image = set_cover_from_url(url)
if image:
book.cover.save(*image, save=False)
book.save()
2021-03-08 18:10:30 +00:00
return redirect("/book/%s" % book.id)
2021-01-13 17:54:35 +00:00
2021-03-08 18:10:30 +00:00
@method_decorator(login_required, name="dispatch")
2021-03-05 01:09:49 +00:00
@method_decorator(
2021-03-08 18:10:30 +00:00
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
)
2021-03-05 01:09:49 +00:00
class ConfirmEditBook(View):
2021-04-26 16:15:42 +00:00
"""confirm edits to a book"""
2021-03-08 18:10:30 +00:00
2021-03-05 01:09:49 +00:00
def post(self, request, book_id=None):
2021-04-26 16:15:42 +00:00
"""edit a book cool"""
2021-03-05 01:09:49 +00:00
# returns None if no match is found
book = models.Edition.objects.filter(id=book_id).first()
form = forms.EditionForm(request.POST, request.FILES, instance=book)
2021-03-08 18:10:30 +00:00
data = {"book": book, "form": form}
2021-03-05 01:09:49 +00:00
if not form.is_valid():
2021-03-18 16:37:16 +00:00
return TemplateResponse(request, "book/edit_book.html", data)
2021-03-05 01:09:49 +00:00
2021-03-08 17:28:22 +00:00
with transaction.atomic():
# save book
book = form.save()
# get or create author as needed
2021-03-14 02:09:09 +00:00
for i in range(int(request.POST.get("author-match-count", 0))):
match = request.POST.get("author_match-%d" % i)
if not match:
return HttpResponseBadRequest()
try:
# if it's an int, it's an ID
match = int(match)
author = get_object_or_404(
models.Author, id=request.POST["author_match-%d" % i]
)
except ValueError:
# otherwise it's a name
author = models.Author.objects.create(name=match)
book.authors.add(author)
2021-03-08 17:28:22 +00:00
# create work, if needed
if not book_id:
2021-03-08 18:10:30 +00:00
work_match = request.POST.get("parent_work")
2021-03-11 23:41:12 +00:00
if work_match and work_match != "0":
2021-03-08 17:28:22 +00:00
work = get_object_or_404(models.Work, id=work_match)
else:
2021-03-08 18:48:45 +00:00
work = models.Work.objects.create(title=form.cleaned_data["title"])
2021-03-08 17:28:22 +00:00
work.authors.set(book.authors.all())
book.parent_work = work
# we don't tell the world when creating a book
book.save(broadcast=False)
2021-03-08 17:28:22 +00:00
2021-03-08 18:10:30 +00:00
for author_id in request.POST.getlist("remove_authors"):
2021-03-08 17:28:22 +00:00
book.authors.remove(author_id)
2021-03-07 23:14:57 +00:00
2021-03-08 16:49:10 +00:00
return redirect("/book/%s" % book.id)
2021-03-05 01:09:49 +00:00
2021-01-13 17:54:35 +00:00
class Editions(View):
2021-04-26 16:15:42 +00:00
"""list of editions"""
2021-03-08 16:49:10 +00:00
2021-01-13 17:54:35 +00:00
def get(self, request, book_id):
2021-04-26 16:15:42 +00:00
"""list of editions of a book"""
2021-01-13 17:54:35 +00:00
work = get_object_or_404(models.Work, id=book_id)
if is_api_request(request):
return ActivitypubResponse(work.to_edition_list(**request.GET))
2021-03-29 18:13:23 +00:00
filters = {}
if request.GET.get("language"):
filters["languages__contains"] = [request.GET.get("language")]
if request.GET.get("format"):
filters["physical_format__iexact"] = request.GET.get("format")
2021-04-20 19:31:45 +00:00
editions = work.editions.order_by("-edition_rank")
2021-03-29 17:58:35 +00:00
languages = set(sum([e.languages for e in editions], []))
2021-01-13 17:54:35 +00:00
2021-04-20 19:31:45 +00:00
paginated = Paginator(editions.filter(**filters), PAGE_LENGTH)
2021-01-13 17:54:35 +00:00
data = {
2021-04-19 22:01:20 +00:00
"editions": paginated.get_page(request.GET.get("page")),
2021-03-08 16:49:10 +00:00
"work": work,
2021-03-29 17:58:35 +00:00
"languages": languages,
2021-03-29 18:38:14 +00:00
"formats": set(
e.physical_format.lower() for e in editions if e.physical_format
),
2021-01-13 17:54:35 +00:00
}
2021-03-29 17:21:48 +00:00
return TemplateResponse(request, "book/editions.html", data)
2021-01-13 17:54:35 +00:00
@login_required
@require_POST
def upload_cover(request, book_id):
2021-04-26 16:15:42 +00:00
"""upload a new cover"""
2021-01-13 17:54:35 +00:00
book = get_object_or_404(models.Edition, id=book_id)
book.last_edited_by = request.user
2021-01-13 17:54:35 +00:00
2021-03-18 17:03:53 +00:00
url = request.POST.get("cover-url")
if url:
image = set_cover_from_url(url)
if image:
book.cover.save(*image)
return redirect("{:s}?cover_error=True".format(book.local_path))
2021-03-18 17:03:53 +00:00
2021-01-13 17:54:35 +00:00
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid() or not form.files.get("cover"):
return redirect(book.local_path)
2021-01-13 17:54:35 +00:00
2021-03-08 16:49:10 +00:00
book.cover = form.files["cover"]
2021-01-13 17:54:35 +00:00
book.save()
return redirect(book.local_path)
2021-01-13 17:54:35 +00:00
def set_cover_from_url(url):
2021-04-26 16:15:42 +00:00
"""load it from a url"""
2021-05-20 23:03:14 +00:00
try:
image_file = get_image(url)
except: # pylint: disable=bare-except
return None
if not image_file:
return None
image_name = str(uuid4()) + "." + url.split(".")[-1]
image_content = ContentFile(image_file.content)
return [image_name, image_content]
2021-01-13 17:54:35 +00:00
@login_required
@require_POST
2021-03-08 16:49:10 +00:00
@permission_required("bookwyrm.edit_book", raise_exception=True)
2021-01-13 17:54:35 +00:00
def add_description(request, book_id):
2021-04-26 16:15:42 +00:00
"""upload a new cover"""
2021-03-08 16:49:10 +00:00
if not request.method == "POST":
return redirect("/")
2021-01-13 17:54:35 +00:00
book = get_object_or_404(models.Edition, id=book_id)
2021-03-08 16:49:10 +00:00
description = request.POST.get("description")
2021-01-13 17:54:35 +00:00
book.description = description
2021-02-28 21:40:57 +00:00
book.last_edited_by = request.user
2021-01-13 17:54:35 +00:00
book.save()
2021-03-08 16:49:10 +00:00
return redirect("/book/%s" % book.id)
2021-01-13 17:54:35 +00:00
@require_POST
def resolve_book(request):
2021-04-26 16:15:42 +00:00
"""figure out the local path to a book from a remote_id"""
2021-03-08 16:49:10 +00:00
remote_id = request.POST.get("remote_id")
2021-01-13 17:54:35 +00:00
connector = connector_manager.get_or_create_connector(remote_id)
book = connector.get_or_create_book(remote_id)
2021-03-08 16:49:10 +00:00
return redirect("/book/%d" % book.id)
2021-01-13 17:54:35 +00:00
@login_required
@require_POST
@transaction.atomic
def switch_edition(request):
2021-04-26 16:15:42 +00:00
"""switch your copy of a book to a different edition"""
2021-03-08 16:49:10 +00:00
edition_id = request.POST.get("edition")
2021-01-13 17:54:35 +00:00
new_edition = get_object_or_404(models.Edition, id=edition_id)
shelfbooks = models.ShelfBook.objects.filter(
2021-03-08 16:49:10 +00:00
book__parent_work=new_edition.parent_work, shelf__user=request.user
2021-01-13 17:54:35 +00:00
)
for shelfbook in shelfbooks.all():
with transaction.atomic():
models.ShelfBook.objects.create(
created_date=shelfbook.created_date,
user=shelfbook.user,
shelf=shelfbook.shelf,
2021-03-08 16:49:10 +00:00
book=new_edition,
)
shelfbook.delete()
2021-01-13 17:54:35 +00:00
readthroughs = models.ReadThrough.objects.filter(
2021-03-08 16:49:10 +00:00
book__parent_work=new_edition.parent_work, user=request.user
2021-01-13 17:54:35 +00:00
)
for readthrough in readthroughs.all():
readthrough.book = new_edition
readthrough.save()
2021-03-08 16:49:10 +00:00
return redirect("/book/%d" % new_edition.id)