forked from mirrors/bookwyrm
Add editions view
This commit is contained in:
parent
1d99e455e8
commit
c67f92af46
7 changed files with 196 additions and 77 deletions
|
@ -264,17 +264,40 @@ class FileLinkForm(CustomForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditionFromWorkForm(CustomForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# make all fields hidden
|
||||||
|
for visible in self.visible_fields():
|
||||||
|
visible.field.widget = forms.HiddenInput()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Work
|
||||||
|
fields = [
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"authors",
|
||||||
|
"description",
|
||||||
|
"languages",
|
||||||
|
"series",
|
||||||
|
"series_number",
|
||||||
|
"subjects",
|
||||||
|
"subject_places",
|
||||||
|
"cover",
|
||||||
|
"first_published_date",
|
||||||
|
]
|
||||||
|
|
||||||
class EditionForm(CustomForm):
|
class EditionForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Edition
|
model = models.Edition
|
||||||
exclude = [
|
exclude = [
|
||||||
|
"authors",
|
||||||
|
"parent_work",
|
||||||
"remote_id",
|
"remote_id",
|
||||||
"origin_id",
|
"origin_id",
|
||||||
"created_date",
|
"created_date",
|
||||||
"updated_date",
|
"updated_date",
|
||||||
"edition_rank",
|
"edition_rank",
|
||||||
"authors",
|
|
||||||
"parent_work",
|
|
||||||
"shelves",
|
"shelves",
|
||||||
"connector",
|
"connector",
|
||||||
"search_vector",
|
"search_vector",
|
||||||
|
|
|
@ -3,18 +3,24 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
{% block title %}
|
||||||
|
{% if book.title %}
|
||||||
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Add Book" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<header class="block">
|
<header class="block">
|
||||||
<h1 class="title level-left">
|
<h1 class="title level-left">
|
||||||
{% if book %}
|
{% if book.title %}
|
||||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Add Book" %}
|
{% trans "Add Book" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
{% if book %}
|
{% if book.created_date %}
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||||
|
@ -33,7 +39,7 @@
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="block"
|
class="block"
|
||||||
{% if book %}
|
{% if book.id %}
|
||||||
name="edit-book"
|
name="edit-book"
|
||||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -119,7 +125,7 @@
|
||||||
{% if not confirm_mode %}
|
{% if not confirm_mode %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if book %}
|
{% if book.id %}
|
||||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/" class="button" data-back>
|
<a href="/" class="button" data-back>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
|
<input type="hidden" name="parent_work" value="{% firstof book.parent_work.id form.parent_work %}">
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
|
@ -123,8 +125,11 @@
|
||||||
</h2>
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{% if book.authors.exists %}
|
{% if book.authors.exists %}
|
||||||
|
{# preserve authors if the book is unsaved #}
|
||||||
|
{{ form.authors.as_hidden }}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
|
{{ form.authors.as_hidden }}
|
||||||
<div class="is-flex is-justify-content-space-between">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
<label class="label mb-2">
|
<label class="label mb-2">
|
||||||
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %} aria-describedby="desc_remove_author_{{author.id}}">
|
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %} aria-describedby="desc_remove_author_{{author.id}}">
|
||||||
|
|
|
@ -496,7 +496,8 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
|
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
|
||||||
re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
|
re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
|
re_path(r"^create-book/data/?$", views.create_book_from_data, name="create-book-data"),
|
||||||
|
re_path(r"^create-book/?$", views.CreateBook.as_view(), name="create-book"),
|
||||||
re_path(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
|
re_path(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
|
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -37,7 +37,7 @@ from .books.books import (
|
||||||
resolve_book,
|
resolve_book,
|
||||||
)
|
)
|
||||||
from .books.books import update_book_from_remote
|
from .books.books import update_book_from_remote
|
||||||
from .books.edit_book import EditBook, ConfirmEditBook
|
from .books.edit_book import EditBook, ConfirmEditBook, CreateBook, create_book_from_data
|
||||||
from .books.editions import Editions, switch_edition
|
from .books.editions import Editions, switch_edition
|
||||||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
from .books.links import BookFileLinks, AddFileLink, delete_link
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" the good stuff! the books! """
|
""" the good stuff! the books! """
|
||||||
from re import sub
|
from re import sub, findall
|
||||||
from dateutil.parser import parse as dateparse
|
from dateutil.parser import parse as dateparse
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||||
|
@ -31,10 +31,8 @@ from .books import set_cover_from_url
|
||||||
class EditBook(View):
|
class EditBook(View):
|
||||||
"""edit a book"""
|
"""edit a book"""
|
||||||
|
|
||||||
def get(self, request, book_id=None):
|
def get(self, request, book_id):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
book = None
|
|
||||||
if book_id:
|
|
||||||
book = get_edition(book_id)
|
book = get_edition(book_id)
|
||||||
if not book.description:
|
if not book.description:
|
||||||
book.description = book.parent_work.description
|
book.description = book.parent_work.description
|
||||||
|
@ -42,7 +40,7 @@ class EditBook(View):
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
def post(self, request, book_id=None):
|
def post(self, request, book_id):
|
||||||
"""edit a book cool"""
|
"""edit a book cool"""
|
||||||
# returns None if no match is found
|
# returns None if no match is found
|
||||||
book = models.Edition.objects.filter(id=book_id).first()
|
book = models.Edition.objects.filter(id=book_id).first()
|
||||||
|
@ -50,16 +48,139 @@ class EditBook(View):
|
||||||
|
|
||||||
data = {"book": book, "form": form}
|
data = {"book": book, "form": form}
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
|
print("FORM NOT VALID", form.errors)
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
# filter out empty author fields
|
data = add_authors(request, data)
|
||||||
|
|
||||||
|
# either of the above cases requires additional confirmation
|
||||||
|
if data.get("add_author"):
|
||||||
|
# creting 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")
|
||||||
|
data["cover_url"] = request.POST.get("cover-url")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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(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}
|
||||||
|
if not form.is_valid():
|
||||||
|
print(form.errors)
|
||||||
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
|
data = add_authors(request, data)
|
||||||
|
author_text = ", ".join(data.get("add_author", []))
|
||||||
|
|
||||||
|
# check if this is an edition of an existing work
|
||||||
|
data["book_matches"] = book_search.search(
|
||||||
|
f'{form.cleaned_data.get("title")} {author_text}',
|
||||||
|
min_confidence=0.1,
|
||||||
|
)[:5]
|
||||||
|
|
||||||
|
parent_work_id = request.POST.get("parent_work")
|
||||||
|
if not parent_work_id or data.get("add_authors"):
|
||||||
|
# creting 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")
|
||||||
|
data["cover_url"] = request.POST.get("cover-url")
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
formcopy["parent_work"] = parent_work_id
|
||||||
|
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
|
||||||
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
|
book = form.save(commit=False)
|
||||||
|
parent_work = get_object_or_404(models.Work, id=parent_work_id)
|
||||||
|
book.parent_work = parent_work
|
||||||
|
|
||||||
|
if request.POST.get("authors"):
|
||||||
|
author_ids = findall(r"\d+", request.POST["authors"])
|
||||||
|
# django can't parse the authors form element
|
||||||
|
book.authors.add(
|
||||||
|
*models.Author.objects.filter(id__in=author_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 add_authors(request, data):
|
||||||
|
"""helper for adding authors"""
|
||||||
add_author = [author for author in request.POST.getlist("add_author") if author]
|
add_author = [author for author in request.POST.getlist("add_author") if author]
|
||||||
if add_author:
|
if not add_author:
|
||||||
|
return data
|
||||||
|
|
||||||
data["add_author"] = add_author
|
data["add_author"] = add_author
|
||||||
data["author_matches"] = []
|
data["author_matches"] = []
|
||||||
data["isni_matches"] = []
|
data["isni_matches"] = []
|
||||||
|
|
||||||
for author in add_author:
|
for author in add_author:
|
||||||
|
# filter out empty author fields
|
||||||
if not author:
|
if not author:
|
||||||
continue
|
continue
|
||||||
# check for existing authors
|
# check for existing authors
|
||||||
|
@ -98,61 +219,23 @@ class EditBook(View):
|
||||||
"existing_isnis": exists,
|
"existing_isnis": exists,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return data
|
||||||
# we're creating a new book
|
|
||||||
if not book:
|
|
||||||
# check if this is an edition of an existing work
|
|
||||||
author_text = book.author_text if book else add_author
|
|
||||||
data["book_matches"] = book_search.search(
|
|
||||||
f'{form.cleaned_data.get("title")} {author_text}',
|
|
||||||
min_confidence=0.5,
|
|
||||||
)[:5]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# this isn't preserved because it isn't part of the form obj
|
|
||||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
|
||||||
data["cover_url"] = request.POST.get("cover-url")
|
|
||||||
|
|
||||||
# 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
|
|
||||||
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(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}")
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||||
def create_book_from_data(request):
|
def create_book_from_data(request):
|
||||||
"""create a book with starter data"""
|
"""create a book with starter data"""
|
||||||
data = {"form": forms.EditionForm(request.POST)}
|
author_ids = findall(r"\d+", request.POST.get("authors"))
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
book = {
|
||||||
|
"parent_work": {"id": request.POST.get("parent_work")},
|
||||||
|
"authors": models.Author.objects.filter(
|
||||||
|
id__in=author_ids
|
||||||
|
).all(),
|
||||||
|
}
|
||||||
|
|
||||||
|
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(login_required, name="dispatch")
|
||||||
|
@ -211,7 +294,7 @@ class ConfirmEditBook(View):
|
||||||
book.authors.add(author)
|
book.authors.add(author)
|
||||||
|
|
||||||
# create work, if needed
|
# create work, if needed
|
||||||
if not book_id:
|
if not book.parent_work:
|
||||||
work_match = request.POST.get("parent_work")
|
work_match = request.POST.get("parent_work")
|
||||||
if work_match and work_match != "0":
|
if work_match and work_match != "0":
|
||||||
work = get_object_or_404(models.Work, id=work_match)
|
work = get_object_or_404(models.Work, id=work_match)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.template.response import TemplateResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from bookwyrm.views.helpers import is_api_request
|
from bookwyrm.views.helpers import is_api_request
|
||||||
|
@ -61,6 +61,7 @@ class Editions(View):
|
||||||
data = {
|
data = {
|
||||||
"editions": paginated.get_page(request.GET.get("page")),
|
"editions": paginated.get_page(request.GET.get("page")),
|
||||||
"work": work,
|
"work": work,
|
||||||
|
"work_form": forms.EditionFromWorkForm(instance=work),
|
||||||
"languages": languages,
|
"languages": languages,
|
||||||
"formats": set(
|
"formats": set(
|
||||||
e.physical_format.lower() for e in editions if e.physical_format
|
e.physical_format.lower() for e in editions if e.physical_format
|
||||||
|
|
Loading…
Reference in a new issue