mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-06-02 04:59:41 +00:00
543d13af6e
These raises are handled implicitly in the form, so they don't have to be called outright.
329 lines
11 KiB
Python
329 lines
11 KiB
Python
""" book list views"""
|
|
from typing import Optional
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.core.paginator import Paginator
|
|
from django.db import transaction
|
|
from django.db.models import Avg, DecimalField, Q, Max
|
|
from django.db.models.functions import Coalesce
|
|
from django.http import HttpResponseBadRequest, HttpResponse
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import reverse
|
|
from django.utils.decorators import method_decorator
|
|
from django.views import View
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from bookwyrm import book_search, forms, models
|
|
from bookwyrm.activitypub import ActivitypubResponse
|
|
from bookwyrm.settings import PAGE_LENGTH
|
|
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
|
|
|
|
|
# pylint: disable=no-self-use
|
|
class List(View):
|
|
"""book list page"""
|
|
|
|
def get(self, request, list_id, **kwargs):
|
|
"""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.raise_visible_to_user(request.user)
|
|
|
|
if is_api_request(request):
|
|
return ActivitypubResponse(book_list.to_activity(**request.GET))
|
|
|
|
if redirect_option := maybe_redirect_local_path(request, book_list):
|
|
return redirect_option
|
|
|
|
items = book_list.listitem_set.filter(approved=True).prefetch_related(
|
|
"user", "book", "book__authors"
|
|
)
|
|
items = sort_list(request, items)
|
|
|
|
paginated = Paginator(items, PAGE_LENGTH)
|
|
|
|
page = paginated.get_page(request.GET.get("page"))
|
|
|
|
embed_key = str(book_list.embed_key.hex)
|
|
embed_url = reverse("embed-list", args=[book_list.id, embed_key])
|
|
embed_url = request.build_absolute_uri(embed_url)
|
|
|
|
if request.GET:
|
|
embed_url = f"{embed_url}?{request.GET.urlencode()}"
|
|
|
|
query = request.GET.get("q", "")
|
|
data = {
|
|
"list": book_list,
|
|
"items": page,
|
|
"page_range": paginated.get_elided_page_range(
|
|
page.number, on_each_side=2, on_ends=1
|
|
),
|
|
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
|
"list_form": forms.ListForm(instance=book_list),
|
|
"query": query,
|
|
"sort_form": forms.SortListForm(request.GET),
|
|
"embed_url": embed_url,
|
|
"add_failed": add_failed,
|
|
"add_succeeded": add_succeeded,
|
|
}
|
|
|
|
if request.user.is_authenticated:
|
|
data["suggested_books"] = get_list_suggestions(
|
|
book_list, request.user, query=query
|
|
)
|
|
return TemplateResponse(request, "lists/list.html", data)
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
def post(self, request, list_id):
|
|
"""edit a list"""
|
|
book_list = get_object_or_404(models.List, id=list_id)
|
|
|
|
form = forms.ListForm(request.POST, instance=book_list)
|
|
if not form.is_valid():
|
|
# this shouldn't happen
|
|
raise Exception(form.errors)
|
|
book_list = form.save(request)
|
|
if not book_list.curation == "group":
|
|
book_list.group = None
|
|
book_list.save(broadcast=False)
|
|
|
|
return redirect(book_list.local_path)
|
|
|
|
|
|
def get_list_suggestions(book_list, user, query=None):
|
|
"""What books might a user want to add to a list"""
|
|
if query:
|
|
# search for books
|
|
return book_search.search(
|
|
query,
|
|
filters=[~Q(parent_work__editions__in=book_list.books.all())],
|
|
)
|
|
# just suggest whatever books are nearby
|
|
suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all()))
|
|
suggestions = [s.book for s in suggestions[:5]]
|
|
if len(suggestions) < 5:
|
|
suggestions += [
|
|
s.default_edition
|
|
for s in models.Work.objects.filter(
|
|
~Q(editions__in=book_list.books.all()),
|
|
).order_by("-updated_date")
|
|
][: 5 - len(suggestions)]
|
|
return suggestions
|
|
|
|
|
|
def sort_list(request, items):
|
|
"""helper to handle the surprisngly involved sorting"""
|
|
# sort_by shall be "order" unless a valid alternative is given
|
|
sort_by = request.GET.get("sort_by", "order")
|
|
if sort_by not in ("order", "title", "rating"):
|
|
sort_by = "order"
|
|
|
|
# direction shall be "ascending" unless a valid alternative is given
|
|
direction = request.GET.get("direction", "ascending")
|
|
if direction not in ("ascending", "descending"):
|
|
direction = "ascending"
|
|
|
|
directional_sort_by = {
|
|
"order": "order",
|
|
"title": "book__title",
|
|
"rating": "average_rating",
|
|
}[sort_by]
|
|
if direction == "descending":
|
|
directional_sort_by = "-" + directional_sort_by
|
|
|
|
if sort_by == "rating":
|
|
items = items.annotate(
|
|
average_rating=Avg(
|
|
Coalesce("book__review__rating", 0.0),
|
|
output_field=DecimalField(),
|
|
)
|
|
)
|
|
return items.order_by(directional_sort_by)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def save_list(request, list_id):
|
|
"""save a list"""
|
|
book_list = get_object_or_404(models.List, id=list_id)
|
|
request.user.saved_lists.add(book_list)
|
|
return redirect("list", list_id)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def unsave_list(request, list_id):
|
|
"""unsave a list"""
|
|
book_list = get_object_or_404(models.List, id=list_id)
|
|
request.user.saved_lists.remove(book_list)
|
|
return redirect("list", list_id)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def delete_list(request, list_id):
|
|
"""delete a list"""
|
|
book_list = get_object_or_404(models.List, id=list_id)
|
|
|
|
# only the owner or a moderator can delete a list
|
|
book_list.raise_not_deletable(request.user)
|
|
|
|
book_list.delete()
|
|
return redirect("lists")
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@transaction.atomic
|
|
def add_book(request):
|
|
"""put a book on a list"""
|
|
book_list = get_object_or_404(models.List, id=request.POST.get("book_list"))
|
|
# make sure the user is allowed to submit to this list
|
|
book_list.raise_visible_to_user(request.user)
|
|
if request.user != book_list.user and book_list.curation == "closed":
|
|
raise PermissionDenied()
|
|
|
|
is_group_member = models.GroupMember.objects.filter(
|
|
group=book_list.group, user=request.user
|
|
).exists()
|
|
|
|
form = forms.ListItemForm(request.POST)
|
|
if not form.is_valid():
|
|
return List().get(request, book_list.id, add_failed=True)
|
|
|
|
item = form.save(request, commit=False)
|
|
|
|
if book_list.curation == "curated":
|
|
# make a pending entry at the end of the list
|
|
order_max = (book_list.listitem_set.aggregate(Max("order"))["order__max"]) or 0
|
|
item.approved = is_group_member or request.user == book_list.user
|
|
else:
|
|
# add the book at the latest order of approved books, before pending books
|
|
order_max = (
|
|
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
|
"order__max"
|
|
]
|
|
) or 0
|
|
increment_order_in_reverse(book_list.id, order_max + 1)
|
|
item.order = order_max + 1
|
|
item.save()
|
|
|
|
return List().get(request, book_list.id, add_succeeded=True)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def remove_book(request, list_id):
|
|
"""remove a book from a list"""
|
|
|
|
book_list = get_object_or_404(models.List, id=list_id)
|
|
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
|
|
|
item.raise_not_deletable(request.user)
|
|
|
|
with transaction.atomic():
|
|
deleted_order = item.order
|
|
item.delete()
|
|
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
|
|
|
return redirect("list", list_id)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def set_book_position(request, list_item_id):
|
|
"""
|
|
Action for when the list user manually specifies a list position, takes
|
|
special care with the unique ordering per list.
|
|
"""
|
|
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
|
try:
|
|
int_position = int(request.POST.get("position"))
|
|
except ValueError:
|
|
return HttpResponseBadRequest("bad value for position. should be an integer")
|
|
|
|
if int_position < 1:
|
|
return HttpResponseBadRequest("position cannot be less than 1")
|
|
|
|
book_list = list_item.book_list
|
|
|
|
# the max position to which a book may be set is the highest order for
|
|
# books which are approved
|
|
order_max = book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
|
"order__max"
|
|
]
|
|
|
|
int_position = min(int_position, order_max)
|
|
|
|
original_order = list_item.order
|
|
if original_order == int_position:
|
|
# no change
|
|
return HttpResponse(status=204)
|
|
|
|
with transaction.atomic():
|
|
if original_order > int_position:
|
|
list_item.order = -1
|
|
list_item.save()
|
|
increment_order_in_reverse(book_list.id, int_position, original_order)
|
|
else:
|
|
list_item.order = -1
|
|
list_item.save()
|
|
decrement_order(book_list.id, original_order, int_position)
|
|
|
|
list_item.order = int_position
|
|
list_item.save()
|
|
|
|
return redirect("list", book_list.id)
|
|
|
|
|
|
@transaction.atomic
|
|
def increment_order_in_reverse(
|
|
book_list_id: int, start: int, end: Optional[int] = None
|
|
):
|
|
"""increase the order number for every item in a list"""
|
|
try:
|
|
book_list = models.List.objects.get(id=book_list_id)
|
|
except models.List.DoesNotExist:
|
|
return
|
|
items = book_list.listitem_set.filter(order__gte=start)
|
|
if end is not None:
|
|
items = items.filter(order__lt=end)
|
|
items = items.order_by("-order")
|
|
for item in items:
|
|
item.order += 1
|
|
item.save()
|
|
|
|
|
|
@transaction.atomic
|
|
def decrement_order(book_list_id, start, end):
|
|
"""decrement the order value for every item in a list"""
|
|
try:
|
|
book_list = models.List.objects.get(id=book_list_id)
|
|
except models.List.DoesNotExist:
|
|
return
|
|
items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by(
|
|
"order"
|
|
)
|
|
for item in items:
|
|
item.order -= 1
|
|
item.save()
|
|
|
|
|
|
@transaction.atomic
|
|
def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
|
|
"""gives each book in a list the proper sequential order number"""
|
|
try:
|
|
book_list = models.List.objects.get(id=book_list_id)
|
|
except models.List.DoesNotExist:
|
|
return
|
|
items = book_list.listitem_set.filter(order__gt=start).order_by("order")
|
|
for i, item in enumerate(items, start):
|
|
effective_order = i + add_offset
|
|
if item.order != effective_order:
|
|
item.order = effective_order
|
|
item.save()
|