mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-05-16 23:43:04 +00:00
d9f6449767
It's confusing to edit a book when this isn't set, so this provides the best-guess version of the sort title if there isn't one provided, and allows the user to change it as needed.
318 lines
12 KiB
Python
318 lines
12 KiB
Python
""" 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
|
|
from django.db import transaction
|
|
from django.http import HttpResponseBadRequest
|
|
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.decorators.http import require_POST
|
|
from django.views import View
|
|
|
|
from bookwyrm import book_search, forms, models
|
|
|
|
# from bookwyrm.activitypub.base_activity import ActivityObject
|
|
from bookwyrm.utils.isni import (
|
|
find_authors_by_name,
|
|
build_author_from_isni,
|
|
augment_author_metadata,
|
|
)
|
|
from bookwyrm.views.helpers import get_edition
|
|
from .books import set_cover_from_url
|
|
|
|
# pylint: disable=no-self-use
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(
|
|
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
|
)
|
|
class EditBook(View):
|
|
"""edit a book"""
|
|
|
|
def get(self, request, book_id):
|
|
"""info about a book"""
|
|
book = get_edition(book_id)
|
|
# This doesn't update the sort title, just pre-populates it in the form
|
|
if book.sort_title in ["", None]:
|
|
book.sort_title = book.guess_sort_title()
|
|
if not book.description:
|
|
book.description = book.parent_work.description
|
|
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
def post(self, request, book_id):
|
|
"""edit a book cool"""
|
|
book = get_object_or_404(models.Edition, id=book_id)
|
|
|
|
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
|
|
|
data = {"book": book, "form": form}
|
|
ensure_transient_values_persist(request, data)
|
|
if not form.is_valid():
|
|
ensure_transient_values_persist(request, data, add_author=True)
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
data = add_authors(request, data)
|
|
|
|
# either of the above cases requires additional confirmation
|
|
if data.get("add_author"):
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
remove_authors = request.POST.getlist("remove_authors")
|
|
for author_id in remove_authors:
|
|
book.authors.remove(author_id)
|
|
|
|
book = form.save(request, 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()
|
|
return redirect(f"/book/{book.id}")
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(
|
|
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
|
)
|
|
class CreateBook(View):
|
|
"""brand new book"""
|
|
|
|
def get(self, request):
|
|
"""info about a book"""
|
|
data = {"form": forms.EditionForm()}
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
# pylint: disable=too-many-locals
|
|
def post(self, request):
|
|
"""create a new book"""
|
|
# returns None if no match is found
|
|
form = forms.EditionForm(request.POST, request.FILES)
|
|
data = {"form": form}
|
|
|
|
# collect data provided by the work or import item
|
|
parent_work_id = request.POST.get("parent_work")
|
|
authors = None
|
|
if request.POST.get("authors"):
|
|
author_ids = findall(r"\d+", request.POST["authors"])
|
|
authors = models.Author.objects.filter(id__in=author_ids)
|
|
|
|
# fake book in case we need to keep editing
|
|
if parent_work_id:
|
|
data["book"] = {
|
|
"parent_work": {"id": parent_work_id},
|
|
"authors": authors,
|
|
}
|
|
|
|
if not form.is_valid():
|
|
ensure_transient_values_persist(request, data, form=form)
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
# we have to call this twice because it requires form.cleaned_data
|
|
# which only exists after we validate the form
|
|
ensure_transient_values_persist(request, data, form=form)
|
|
data = add_authors(request, data)
|
|
|
|
# check if this is an edition of an existing work
|
|
author_text = ", ".join(data.get("add_author", []))
|
|
data["book_matches"] = book_search.search(
|
|
f'{form.cleaned_data.get("title")} {author_text}',
|
|
min_confidence=0.1,
|
|
)[:5]
|
|
|
|
# go to confirm mode
|
|
if not parent_work_id or data.get("add_author"):
|
|
data["confirm_mode"] = True
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
with transaction.atomic():
|
|
book = form.save(request)
|
|
parent_work = get_object_or_404(models.Work, id=parent_work_id)
|
|
book.parent_work = parent_work
|
|
|
|
if authors:
|
|
book.authors.add(*authors)
|
|
|
|
url = request.POST.get("cover-url")
|
|
if url:
|
|
image = set_cover_from_url(url)
|
|
if image:
|
|
book.cover.save(*image, save=False)
|
|
|
|
book.save()
|
|
return redirect(f"/book/{book.id}")
|
|
|
|
|
|
def ensure_transient_values_persist(request, data, **kwargs):
|
|
"""ensure that values of transient form fields persist when re-rendering the form"""
|
|
data["cover_url"] = request.POST.get("cover-url")
|
|
if kwargs and kwargs.get("form"):
|
|
data["book"] = data.get("book") or {}
|
|
data["book"]["subjects"] = kwargs["form"].cleaned_data["subjects"]
|
|
data["add_author"] = request.POST.getlist("add_author")
|
|
elif kwargs and kwargs.get("add_author") is True:
|
|
data["add_author"] = request.POST.getlist("add_author")
|
|
|
|
|
|
def add_authors(request, data):
|
|
"""helper for adding authors"""
|
|
add_author = [author for author in request.POST.getlist("add_author") if author]
|
|
if not add_author:
|
|
data["add_author"] = []
|
|
return data
|
|
|
|
data["add_author"] = add_author
|
|
data["author_matches"] = []
|
|
data["isni_matches"] = []
|
|
|
|
# creating a book or adding an author to a book needs another step
|
|
data["confirm_mode"] = True
|
|
# this isn't preserved because it isn't part of the form obj
|
|
data["remove_authors"] = request.POST.getlist("remove_authors")
|
|
|
|
for author in add_author:
|
|
# filter out empty author fields
|
|
if not author:
|
|
continue
|
|
# check for existing authors
|
|
vector = SearchVector("name", weight="A") + SearchVector("aliases", weight="B")
|
|
|
|
author_matches = (
|
|
models.Author.objects.annotate(search=vector)
|
|
.annotate(rank=SearchRank(vector, author))
|
|
.filter(rank__gt=0.4)
|
|
.order_by("-rank")[:5]
|
|
)
|
|
|
|
isni_authors = find_authors_by_name(
|
|
author, description=True
|
|
) # find matches from ISNI API
|
|
|
|
# dedupe isni authors we already have in the DB
|
|
exists = [
|
|
i
|
|
for i in isni_authors
|
|
for a in author_matches
|
|
if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
|
|
]
|
|
|
|
# pylint: disable=cell-var-from-loop
|
|
matches = list(filter(lambda x: x not in exists, isni_authors))
|
|
# combine existing and isni authors
|
|
matches.extend(author_matches)
|
|
|
|
data["author_matches"].append(
|
|
{
|
|
"name": author.strip(),
|
|
"matches": matches,
|
|
"existing_isnis": exists,
|
|
}
|
|
)
|
|
return data
|
|
|
|
|
|
@require_POST
|
|
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
|
def create_book_from_data(request):
|
|
"""create a book with starter data"""
|
|
author_ids = findall(r"\d+", request.POST.get("authors"))
|
|
book = {
|
|
"parent_work": {"id": request.POST.get("parent_work")},
|
|
"authors": models.Author.objects.filter(id__in=author_ids).all(),
|
|
"subjects": request.POST.getlist("subjects"),
|
|
}
|
|
|
|
data = {"book": book, "form": forms.EditionForm(request.POST)}
|
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
|
|
|
|
|
@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"""
|
|
|
|
# pylint: disable=too-many-locals
|
|
# pylint: disable=too-many-branches
|
|
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, "book/edit/edit_book.html", data)
|
|
|
|
with transaction.atomic():
|
|
# save book
|
|
book = form.save(request)
|
|
|
|
# add known authors
|
|
authors = None
|
|
if request.POST.get("authors"):
|
|
author_ids = findall(r"\d+", request.POST["authors"])
|
|
authors = models.Author.objects.filter(id__in=author_ids)
|
|
book.authors.add(*authors)
|
|
|
|
# get or create author as needed
|
|
for i in range(int(request.POST.get("author-match-count", 0))):
|
|
match = request.POST.get(f"author_match-{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[f"author_match-{i}"]
|
|
)
|
|
# update author metadata if the ISNI record is more complete
|
|
isni = request.POST.get(f"isni-for-{match}", None)
|
|
if isni is not None:
|
|
augment_author_metadata(author, isni)
|
|
except ValueError:
|
|
# otherwise it's a new author
|
|
isni_match = request.POST.get(f"author_match-{i}")
|
|
author_object = build_author_from_isni(isni_match)
|
|
# with author data class from isni id
|
|
if "author" in author_object:
|
|
skeleton = models.Author.objects.create(
|
|
name=author_object["author"].name
|
|
)
|
|
author = author_object["author"].to_model(
|
|
model=models.Author, overwrite=True, instance=skeleton
|
|
)
|
|
else:
|
|
# or it's just a name
|
|
author = models.Author.objects.create(name=match)
|
|
book.authors.add(author)
|
|
|
|
# create work, if needed
|
|
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)
|
|
else:
|
|
work = models.Work.objects.create(title=form.cleaned_data["title"])
|
|
work.authors.set(book.authors.all())
|
|
book.parent_work = work
|
|
|
|
for author_id in request.POST.getlist("remove_authors"):
|
|
book.authors.remove(author_id)
|
|
|
|
# import cover, if requested
|
|
url = request.POST.get("cover-url")
|
|
if url:
|
|
image = set_cover_from_url(url)
|
|
if image:
|
|
book.cover.save(*image, save=False)
|
|
|
|
# we don't tell the world when creating a book
|
|
book.save(broadcast=False)
|
|
|
|
return redirect(f"/book/{book.id}")
|