moviewyrm/bookwyrm/views/books.py

315 lines
11 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" the good stuff! the books! """
2021-01-13 17:54:35 +00:00
from django.core.paginator import Paginator
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
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
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
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 bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_activity_feed, get_edition
2021-02-15 19:30:22 +00:00
from .helpers import privacy_filter
2021-01-13 17:54:35 +00:00
# pylint: disable= no-self-use
class Book(View):
2021-03-08 16:49:10 +00:00
""" a book! this is the stuff """
2021-01-13 17:54:35 +00:00
def get(self, request, book_id):
2021-03-08 16:49:10 +00:00
""" info about a book """
2021-01-13 17:54:35 +00:00
try:
2021-03-08 16:49:10 +00:00
page = int(request.GET.get("page", 1))
2021-01-13 17:54:35 +00:00
except ValueError:
page = 1
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.get_default_edition()
if not book:
return HttpResponseNotFound()
work = book.parent_work
if not work:
return HttpResponseNotFound()
# all reviews for the book
reviews = models.Review.objects.filter(book__in=work.editions.all())
reviews = get_activity_feed(request.user, queryset=reviews)
2021-01-13 17:54:35 +00:00
# the reviews to show
2021-03-08 16:49:10 +00:00
paginated = Paginator(
reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH
)
2021-01-13 17:54:35 +00:00
reviews_page = paginated.page(page)
user_tags = readthroughs = user_shelves = other_edition_shelves = []
if request.user.is_authenticated:
user_tags = models.UserTag.objects.filter(
book=book, user=request.user
2021-03-08 16:49:10 +00:00
).values_list("tag__identifier", flat=True)
2021-01-13 17:54:35 +00:00
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-03-08 16:49:10 +00:00
user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book)
2021-01-13 17:54:35 +00:00
other_edition_shelves = models.ShelfBook.objects.filter(
~Q(book=book),
user=request.user,
2021-01-13 17:54:35 +00:00
book__parent_work=book.parent_work,
)
data = {
2021-03-08 16:49:10 +00:00
"book": book,
"reviews": reviews_page,
"review_count": reviews.count(),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"tags": models.UserTag.objects.filter(book=book),
"lists": privacy_filter(
request.user, book.list_set.filter(listitem__approved=True)
2021-02-24 19:35:19 +00:00
),
2021-03-08 16:49:10 +00:00
"user_tags": user_tags,
"user_shelves": user_shelves,
"other_edition_shelves": other_edition_shelves,
"readthroughs": readthroughs,
"path": "/book/%s" % book_id,
2021-01-13 17:54:35 +00:00
}
2021-03-08 16:49:10 +00:00
return TemplateResponse(request, "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):
''' edit a book '''
def get(self, request, book_id=None):
2021-01-13 17:54:35 +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-01-13 17:54:35 +00:00
data = {
'book': book,
'form': forms.EditionForm(instance=book)
}
return TemplateResponse(request, 'edit_book.html', data)
def post(self, request, book_id=None):
2021-01-13 17:54:35 +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)
data = {
'book': book,
'form': form
}
2021-01-13 17:54:35 +00:00
if not form.is_valid():
return TemplateResponse(request, 'edit_book.html', data)
2021-03-04 21:48:50 +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-04 21:48:50 +00:00
data['add_author'] = add_author
# check for existing authors
vector = SearchVector('name', weight='A') +\
SearchVector('aliases', weight='B')
data['author_matches'] = models.Author.objects.annotate(
search=vector
).annotate(
rank=SearchRank(vector, add_author)
2021-03-08 17:28:22 +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
data['book_matches'] = connector_manager.local_search(
'%s %s' % (form.cleaned_data.get('title'), author_text),
min_confidence=0.5,
raw=True
)[: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
data['confirm_mode'] = True
2021-03-04 21:48:50 +00:00
return TemplateResponse(request, 'edit_book.html', data)
2021-01-13 17:54:35 +00:00
2021-03-07 22:19:22 +00:00
remove_authors = request.POST.getlist('remove_authors')
for author_id in remove_authors:
book.authors.remove(author_id)
2021-03-04 21:48:50 +00:00
book = form.save()
2021-01-13 17:54:35 +00:00
return redirect('/book/%s' % book.id)
2021-03-05 01:09:49 +00:00
@method_decorator(login_required, name='dispatch')
@method_decorator(
permission_required('bookwyrm.edit_book', raise_exception=True),
name='dispatch')
class ConfirmEditBook(View):
''' confirm edits to a book '''
def post(self, request, book_id=None):
''' edit a book cool '''
# 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)
data = {
'book': book,
'form': form
}
if not form.is_valid():
return TemplateResponse(request, 'edit_book.html', data)
2021-03-08 17:28:22 +00:00
with transaction.atomic():
# save book
book = form.save()
# get or create author as needed
if request.POST.get('add_author'):
if request.POST.get('author_match'):
author = get_object_or_404(
models.Author, id=request.POST['author_match'])
else:
author = models.Author.objects.create(
name=request.POST.get('add_author'))
book.authors.add(author)
# create work, if needed
if not book_id:
work_match = request.POST.get('parent_work')
if work_match:
work = get_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())
book.parent_work = work
book.save()
for author_id in request.POST.getlist('remove_authors'):
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-03-08 16:49:10 +00:00
""" list of editions """
2021-01-13 17:54:35 +00:00
def get(self, request, book_id):
2021-03-08 16:49:10 +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))
data = {
2021-03-08 16:49:10 +00:00
"editions": work.editions.order_by("-edition_rank").all(),
"work": work,
2021-01-13 17:54:35 +00:00
}
2021-03-08 16:49:10 +00:00
return TemplateResponse(request, "editions.html", data)
2021-01-13 17:54:35 +00:00
@login_required
@require_POST
def upload_cover(request, book_id):
2021-03-08 16:49:10 +00:00
""" upload a new cover """
2021-01-13 17:54:35 +00:00
book = get_object_or_404(models.Edition, id=book_id)
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid():
2021-03-08 16:49:10 +00:00
return redirect("/book/%d" % book.id)
2021-01-13 17:54:35 +00:00
2021-02-28 21:40:57 +00:00
book.last_edited_by = request.user
2021-03-08 16:49:10 +00:00
book.cover = form.files["cover"]
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
@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-03-08 16:49:10 +00:00
""" upload a new cover """
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-03-08 16:49:10 +00:00
""" figure out the local path to a book from a remote_id """
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-03-08 16:49:10 +00:00
""" switch your copy of a book to a different edition """
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)