We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
When adding multiple authors to one book, only the first is added
**Describe the bug**

I would like to add multiple authors to a book (it's an anthology). When I add multiple authors to the book, via the "Add Another Author" button, only the first one is added.

**To Reproduce**

1. Edit book
2. Fill the info for one author (Charlie Jane Anders) in the input
3. Click "Add Another Author"
4. Fill the info for the new author in the second input
5. Click "Add Another Author"
6. Fill the info for that new author in that third input
7. Save book
8. The interface shows the message _Is "Charlie Jane Anders" one of these authors?_
9. Select one of the choices
10. Only Charlie Jane Anders has been added, the other authors don't show up.

**Expected behavior**

I would like to see all authors added, and the interface for selecting the right author (ie. _Is "Charlie Jane Anders" one of these authors?_) should show the choices for all the authors I add.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Instance**

boitam.eu (I expect this problem to be global)




--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/views/books/edit_book.py`
Content:
```
1 """ the good stuff! the books! """
2 from re import sub, findall
3 from django.contrib.auth.decorators import login_required, permission_required
4 from django.contrib.postgres.search import SearchRank, SearchVector
5 from django.db import transaction
6 from django.http import HttpResponseBadRequest
7 from django.shortcuts import get_object_or_404, redirect
8 from django.template.response import TemplateResponse
9 from django.utils.decorators import method_decorator
10 from django.views.decorators.http import require_POST
11 from django.views import View
12 
13 from bookwyrm import book_search, forms, models
14 
15 # from bookwyrm.activitypub.base_activity import ActivityObject
16 from bookwyrm.utils.isni import (
17     find_authors_by_name,
18     build_author_from_isni,
19     augment_author_metadata,
20 )
21 from bookwyrm.views.helpers import get_edition
22 from .books import set_cover_from_url
23 
24 # pylint: disable=no-self-use
25 @method_decorator(login_required, name="dispatch")
26 @method_decorator(
27     permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
28 )
29 class EditBook(View):
30     """edit a book"""
31 
32     def get(self, request, book_id):
33         """info about a book"""
34         book = get_edition(book_id)
35         if not book.description:
36             book.description = book.parent_work.description
37         data = {"book": book, "form": forms.EditionForm(instance=book)}
38         return TemplateResponse(request, "book/edit/edit_book.html", data)
39 
40     def post(self, request, book_id):
41         """edit a book cool"""
42         book = get_object_or_404(models.Edition, id=book_id)
43         form = forms.EditionForm(request.POST, request.FILES, instance=book)
44 
45         data = {"book": book, "form": form}
46         if not form.is_valid():
47             return TemplateResponse(request, "book/edit/edit_book.html", data)
48 
49         data = add_authors(request, data)
50 
51         # either of the above cases requires additional confirmation
52         if data.get("add_author"):
53             return TemplateResponse(request, "book/edit/edit_book.html", data)
54 
55         remove_authors = request.POST.getlist("remove_authors")
56         for author_id in remove_authors:
57             book.authors.remove(author_id)
58 
59         book = form.save(commit=False)
60 
61         url = request.POST.get("cover-url")
62         if url:
63             image = set_cover_from_url(url)
64             if image:
65                 book.cover.save(*image, save=False)
66 
67         book.save()
68         return redirect(f"/book/{book.id}")
69 
70 
71 @method_decorator(login_required, name="dispatch")
72 @method_decorator(
73     permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
74 )
75 class CreateBook(View):
76     """brand new book"""
77 
78     def get(self, request):
79         """info about a book"""
80         data = {"form": forms.EditionForm()}
81         return TemplateResponse(request, "book/edit/edit_book.html", data)
82 
83     # pylint: disable=too-many-locals
84     def post(self, request):
85         """create a new book"""
86         # returns None if no match is found
87         form = forms.EditionForm(request.POST, request.FILES)
88         data = {"form": form}
89 
90         # collect data provided by the work or import item
91         parent_work_id = request.POST.get("parent_work")
92         authors = None
93         if request.POST.get("authors"):
94             author_ids = findall(r"\d+", request.POST["authors"])
95             authors = models.Author.objects.filter(id__in=author_ids)
96 
97         # fake book in case we need to keep editing
98         if parent_work_id:
99             data["book"] = {
100                 "parent_work": {"id": parent_work_id},
101                 "authors": authors,
102             }
103 
104         if not form.is_valid():
105             return TemplateResponse(request, "book/edit/edit_book.html", data)
106 
107         data = add_authors(request, data)
108 
109         # check if this is an edition of an existing work
110         author_text = ", ".join(data.get("add_author", []))
111         data["book_matches"] = book_search.search(
112             f'{form.cleaned_data.get("title")} {author_text}',
113             min_confidence=0.1,
114         )[:5]
115 
116         # go to confirm mode
117         if not parent_work_id or data.get("add_author"):
118             return TemplateResponse(request, "book/edit/edit_book.html", data)
119 
120         with transaction.atomic():
121             book = form.save()
122             parent_work = get_object_or_404(models.Work, id=parent_work_id)
123             book.parent_work = parent_work
124 
125             if authors:
126                 book.authors.add(*authors)
127 
128             url = request.POST.get("cover-url")
129             if url:
130                 image = set_cover_from_url(url)
131                 if image:
132                     book.cover.save(*image, save=False)
133 
134             book.save()
135         return redirect(f"/book/{book.id}")
136 
137 
138 def add_authors(request, data):
139     """helper for adding authors"""
140     add_author = [author for author in request.POST.getlist("add_author") if author]
141     if not add_author:
142         return data
143 
144     data["add_author"] = add_author
145     data["author_matches"] = []
146     data["isni_matches"] = []
147 
148     # creting a book or adding an author to a book needs another step
149     data["confirm_mode"] = True
150     # this isn't preserved because it isn't part of the form obj
151     data["remove_authors"] = request.POST.getlist("remove_authors")
152     data["cover_url"] = request.POST.get("cover-url")
153 
154     for author in add_author:
155         # filter out empty author fields
156         if not author:
157             continue
158         # check for existing authors
159         vector = SearchVector("name", weight="A") + SearchVector("aliases", weight="B")
160 
161         author_matches = (
162             models.Author.objects.annotate(search=vector)
163             .annotate(rank=SearchRank(vector, author))
164             .filter(rank__gt=0.4)
165             .order_by("-rank")[:5]
166         )
167 
168         isni_authors = find_authors_by_name(
169             author, description=True
170         )  # find matches from ISNI API
171 
172         # dedupe isni authors we already have in the DB
173         exists = [
174             i
175             for i in isni_authors
176             for a in author_matches
177             if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
178         ]
179 
180         # pylint: disable=cell-var-from-loop
181         matches = list(filter(lambda x: x not in exists, isni_authors))
182         # combine existing and isni authors
183         matches.extend(author_matches)
184 
185         data["author_matches"].append(
186             {
187                 "name": author.strip(),
188                 "matches": matches,
189                 "existing_isnis": exists,
190             }
191         )
192         return data
193 
194 
195 @require_POST
196 @permission_required("bookwyrm.edit_book", raise_exception=True)
197 def create_book_from_data(request):
198     """create a book with starter data"""
199     author_ids = findall(r"\d+", request.POST.get("authors"))
200     book = {
201         "parent_work": {"id": request.POST.get("parent_work")},
202         "authors": models.Author.objects.filter(id__in=author_ids).all(),
203         "subjects": request.POST.getlist("subjects"),
204     }
205 
206     data = {"book": book, "form": forms.EditionForm(request.POST)}
207     return TemplateResponse(request, "book/edit/edit_book.html", data)
208 
209 
210 @method_decorator(login_required, name="dispatch")
211 @method_decorator(
212     permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
213 )
214 class ConfirmEditBook(View):
215     """confirm edits to a book"""
216 
217     # pylint: disable=too-many-locals
218     # pylint: disable=too-many-branches
219     def post(self, request, book_id=None):
220         """edit a book cool"""
221         # returns None if no match is found
222         book = models.Edition.objects.filter(id=book_id).first()
223         form = forms.EditionForm(request.POST, request.FILES, instance=book)
224 
225         data = {"book": book, "form": form}
226         if not form.is_valid():
227             return TemplateResponse(request, "book/edit/edit_book.html", data)
228 
229         with transaction.atomic():
230             # save book
231             book = form.save()
232 
233             # add known authors
234             authors = None
235             if request.POST.get("authors"):
236                 author_ids = findall(r"\d+", request.POST["authors"])
237                 authors = models.Author.objects.filter(id__in=author_ids)
238                 book.authors.add(*authors)
239 
240             # get or create author as needed
241             for i in range(int(request.POST.get("author-match-count", 0))):
242                 match = request.POST.get(f"author_match-{i}")
243                 if not match:
244                     return HttpResponseBadRequest()
245                 try:
246                     # if it's an int, it's an ID
247                     match = int(match)
248                     author = get_object_or_404(
249                         models.Author, id=request.POST[f"author_match-{i}"]
250                     )
251                     # update author metadata if the ISNI record is more complete
252                     isni = request.POST.get(f"isni-for-{match}", None)
253                     if isni is not None:
254                         augment_author_metadata(author, isni)
255                 except ValueError:
256                     # otherwise it's a new author
257                     isni_match = request.POST.get(f"author_match-{i}")
258                     author_object = build_author_from_isni(isni_match)
259                     # with author data class from isni id
260                     if "author" in author_object:
261                         skeleton = models.Author.objects.create(
262                             name=author_object["author"].name
263                         )
264                         author = author_object["author"].to_model(
265                             model=models.Author, overwrite=True, instance=skeleton
266                         )
267                     else:
268                         # or it's just a name
269                         author = models.Author.objects.create(name=match)
270                 book.authors.add(author)
271 
272             # create work, if needed
273             if not book.parent_work:
274                 work_match = request.POST.get("parent_work")
275                 if work_match and work_match != "0":
276                     work = get_object_or_404(models.Work, id=work_match)
277                 else:
278                     work = models.Work.objects.create(title=form.cleaned_data["title"])
279                     work.authors.set(book.authors.all())
280                 book.parent_work = work
281 
282             for author_id in request.POST.getlist("remove_authors"):
283                 book.authors.remove(author_id)
284 
285             # import cover, if requested
286             url = request.POST.get("cover-url")
287             if url:
288                 image = set_cover_from_url(url)
289                 if image:
290                     book.cover.save(*image, save=False)
291 
292             # we don't tell the world when creating a book
293             book.save(broadcast=False)
294 
295         return redirect(f"/book/{book.id}")
296
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py
--- a/bookwyrm/views/books/edit_book.py
+++ b/bookwyrm/views/books/edit_book.py
@@ -189,7 +189,7 @@
                 "existing_isnis": exists,
             }
         )
-        return data
+    return data
 
 
 @require_POST

{"golden_diff": "diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py\n--- a/bookwyrm/views/books/edit_book.py\n+++ b/bookwyrm/views/books/edit_book.py\n@@ -189,7 +189,7 @@\n                 \"existing_isnis\": exists,\n             }\n         )\n-        return data\n+    return data\n \n \n @require_POST\n", "issue": "When adding multiple authors to one book, only the first is added\n**Describe the bug**\r\n\r\nI would like to add multiple authors to a book (it's an anthology). When I add multiple authors to the book, via the \"Add Another Author\" button, only the first one is added.\r\n\r\n**To Reproduce**\r\n\r\n1. Edit book\r\n2. Fill the info for one author (Charlie Jane Anders) in the input\r\n3. Click \"Add Another Author\"\r\n4. Fill the info for the new author in the second input\r\n5. Click \"Add Another Author\"\r\n6. Fill the info for that new author in that third input\r\n7. Save book\r\n8. The interface shows the message _Is \"Charlie Jane Anders\" one of these authors?_\r\n9. Select one of the choices\r\n10. Only Charlie Jane Anders has been added, the other authors don't show up.\r\n\r\n**Expected behavior**\r\n\r\nI would like to see all authors added, and the interface for selecting the right author (ie. _Is \"Charlie Jane Anders\" one of these authors?_) should show the choices for all the authors I add.\r\n\r\n**Screenshots**\r\nIf applicable, add screenshots to help explain your problem.\r\n\r\n**Instance**\r\n\r\nboitam.eu (I expect this problem to be global)\r\n\r\n\r\n\n", "before_files": [{"content": "\"\"\" the good stuff! the books! \"\"\"\nfrom re import sub, findall\nfrom django.contrib.auth.decorators import login_required, permission_required\nfrom django.contrib.postgres.search import SearchRank, SearchVector\nfrom django.db import transaction\nfrom django.http import HttpResponseBadRequest\nfrom django.shortcuts import get_object_or_404, redirect\nfrom django.template.response import TemplateResponse\nfrom django.utils.decorators import method_decorator\nfrom django.views.decorators.http import require_POST\nfrom django.views import View\n\nfrom bookwyrm import book_search, forms, models\n\n# from bookwyrm.activitypub.base_activity import ActivityObject\nfrom bookwyrm.utils.isni import (\n    find_authors_by_name,\n    build_author_from_isni,\n    augment_author_metadata,\n)\nfrom bookwyrm.views.helpers import get_edition\nfrom .books import set_cover_from_url\n\n# pylint: disable=no-self-use\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass EditBook(View):\n    \"\"\"edit a book\"\"\"\n\n    def get(self, request, book_id):\n        \"\"\"info about a book\"\"\"\n        book = get_edition(book_id)\n        if not book.description:\n            book.description = book.parent_work.description\n        data = {\"book\": book, \"form\": forms.EditionForm(instance=book)}\n        return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n    def post(self, request, book_id):\n        \"\"\"edit a book cool\"\"\"\n        book = get_object_or_404(models.Edition, id=book_id)\n        form = forms.EditionForm(request.POST, request.FILES, instance=book)\n\n        data = {\"book\": book, \"form\": form}\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        data = add_authors(request, data)\n\n        # either of the above cases requires additional confirmation\n        if data.get(\"add_author\"):\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        remove_authors = request.POST.getlist(\"remove_authors\")\n        for author_id in remove_authors:\n            book.authors.remove(author_id)\n\n        book = form.save(commit=False)\n\n        url = request.POST.get(\"cover-url\")\n        if url:\n            image = set_cover_from_url(url)\n            if image:\n                book.cover.save(*image, save=False)\n\n        book.save()\n        return redirect(f\"/book/{book.id}\")\n\n\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass CreateBook(View):\n    \"\"\"brand new book\"\"\"\n\n    def get(self, request):\n        \"\"\"info about a book\"\"\"\n        data = {\"form\": forms.EditionForm()}\n        return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n    # pylint: disable=too-many-locals\n    def post(self, request):\n        \"\"\"create a new book\"\"\"\n        # returns None if no match is found\n        form = forms.EditionForm(request.POST, request.FILES)\n        data = {\"form\": form}\n\n        # collect data provided by the work or import item\n        parent_work_id = request.POST.get(\"parent_work\")\n        authors = None\n        if request.POST.get(\"authors\"):\n            author_ids = findall(r\"\\d+\", request.POST[\"authors\"])\n            authors = models.Author.objects.filter(id__in=author_ids)\n\n        # fake book in case we need to keep editing\n        if parent_work_id:\n            data[\"book\"] = {\n                \"parent_work\": {\"id\": parent_work_id},\n                \"authors\": authors,\n            }\n\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        data = add_authors(request, data)\n\n        # check if this is an edition of an existing work\n        author_text = \", \".join(data.get(\"add_author\", []))\n        data[\"book_matches\"] = book_search.search(\n            f'{form.cleaned_data.get(\"title\")} {author_text}',\n            min_confidence=0.1,\n        )[:5]\n\n        # go to confirm mode\n        if not parent_work_id or data.get(\"add_author\"):\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        with transaction.atomic():\n            book = form.save()\n            parent_work = get_object_or_404(models.Work, id=parent_work_id)\n            book.parent_work = parent_work\n\n            if authors:\n                book.authors.add(*authors)\n\n            url = request.POST.get(\"cover-url\")\n            if url:\n                image = set_cover_from_url(url)\n                if image:\n                    book.cover.save(*image, save=False)\n\n            book.save()\n        return redirect(f\"/book/{book.id}\")\n\n\ndef add_authors(request, data):\n    \"\"\"helper for adding authors\"\"\"\n    add_author = [author for author in request.POST.getlist(\"add_author\") if author]\n    if not add_author:\n        return data\n\n    data[\"add_author\"] = add_author\n    data[\"author_matches\"] = []\n    data[\"isni_matches\"] = []\n\n    # creting a book or adding an author to a book needs another step\n    data[\"confirm_mode\"] = True\n    # this isn't preserved because it isn't part of the form obj\n    data[\"remove_authors\"] = request.POST.getlist(\"remove_authors\")\n    data[\"cover_url\"] = request.POST.get(\"cover-url\")\n\n    for author in add_author:\n        # filter out empty author fields\n        if not author:\n            continue\n        # check for existing authors\n        vector = SearchVector(\"name\", weight=\"A\") + SearchVector(\"aliases\", weight=\"B\")\n\n        author_matches = (\n            models.Author.objects.annotate(search=vector)\n            .annotate(rank=SearchRank(vector, author))\n            .filter(rank__gt=0.4)\n            .order_by(\"-rank\")[:5]\n        )\n\n        isni_authors = find_authors_by_name(\n            author, description=True\n        )  # find matches from ISNI API\n\n        # dedupe isni authors we already have in the DB\n        exists = [\n            i\n            for i in isni_authors\n            for a in author_matches\n            if sub(r\"\\D\", \"\", str(i.isni)) == sub(r\"\\D\", \"\", str(a.isni))\n        ]\n\n        # pylint: disable=cell-var-from-loop\n        matches = list(filter(lambda x: x not in exists, isni_authors))\n        # combine existing and isni authors\n        matches.extend(author_matches)\n\n        data[\"author_matches\"].append(\n            {\n                \"name\": author.strip(),\n                \"matches\": matches,\n                \"existing_isnis\": exists,\n            }\n        )\n        return data\n\n\n@require_POST\n@permission_required(\"bookwyrm.edit_book\", raise_exception=True)\ndef create_book_from_data(request):\n    \"\"\"create a book with starter data\"\"\"\n    author_ids = findall(r\"\\d+\", request.POST.get(\"authors\"))\n    book = {\n        \"parent_work\": {\"id\": request.POST.get(\"parent_work\")},\n        \"authors\": models.Author.objects.filter(id__in=author_ids).all(),\n        \"subjects\": request.POST.getlist(\"subjects\"),\n    }\n\n    data = {\"book\": book, \"form\": forms.EditionForm(request.POST)}\n    return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass ConfirmEditBook(View):\n    \"\"\"confirm edits to a book\"\"\"\n\n    # pylint: disable=too-many-locals\n    # pylint: disable=too-many-branches\n    def post(self, request, book_id=None):\n        \"\"\"edit a book cool\"\"\"\n        # returns None if no match is found\n        book = models.Edition.objects.filter(id=book_id).first()\n        form = forms.EditionForm(request.POST, request.FILES, instance=book)\n\n        data = {\"book\": book, \"form\": form}\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        with transaction.atomic():\n            # save book\n            book = form.save()\n\n            # add known authors\n            authors = None\n            if request.POST.get(\"authors\"):\n                author_ids = findall(r\"\\d+\", request.POST[\"authors\"])\n                authors = models.Author.objects.filter(id__in=author_ids)\n                book.authors.add(*authors)\n\n            # get or create author as needed\n            for i in range(int(request.POST.get(\"author-match-count\", 0))):\n                match = request.POST.get(f\"author_match-{i}\")\n                if not match:\n                    return HttpResponseBadRequest()\n                try:\n                    # if it's an int, it's an ID\n                    match = int(match)\n                    author = get_object_or_404(\n                        models.Author, id=request.POST[f\"author_match-{i}\"]\n                    )\n                    # update author metadata if the ISNI record is more complete\n                    isni = request.POST.get(f\"isni-for-{match}\", None)\n                    if isni is not None:\n                        augment_author_metadata(author, isni)\n                except ValueError:\n                    # otherwise it's a new author\n                    isni_match = request.POST.get(f\"author_match-{i}\")\n                    author_object = build_author_from_isni(isni_match)\n                    # with author data class from isni id\n                    if \"author\" in author_object:\n                        skeleton = models.Author.objects.create(\n                            name=author_object[\"author\"].name\n                        )\n                        author = author_object[\"author\"].to_model(\n                            model=models.Author, overwrite=True, instance=skeleton\n                        )\n                    else:\n                        # or it's just a name\n                        author = models.Author.objects.create(name=match)\n                book.authors.add(author)\n\n            # create work, if needed\n            if not book.parent_work:\n                work_match = request.POST.get(\"parent_work\")\n                if work_match and work_match != \"0\":\n                    work = get_object_or_404(models.Work, id=work_match)\n                else:\n                    work = models.Work.objects.create(title=form.cleaned_data[\"title\"])\n                    work.authors.set(book.authors.all())\n                book.parent_work = work\n\n            for author_id in request.POST.getlist(\"remove_authors\"):\n                book.authors.remove(author_id)\n\n            # import cover, if requested\n            url = request.POST.get(\"cover-url\")\n            if url:\n                image = set_cover_from_url(url)\n                if image:\n                    book.cover.save(*image, save=False)\n\n            # we don't tell the world when creating a book\n            book.save(broadcast=False)\n\n        return redirect(f\"/book/{book.id}\")\n", "path": "bookwyrm/views/books/edit_book.py"}], "after_files": [{"content": "\"\"\" the good stuff! the books! \"\"\"\nfrom re import sub, findall\nfrom django.contrib.auth.decorators import login_required, permission_required\nfrom django.contrib.postgres.search import SearchRank, SearchVector\nfrom django.db import transaction\nfrom django.http import HttpResponseBadRequest\nfrom django.shortcuts import get_object_or_404, redirect\nfrom django.template.response import TemplateResponse\nfrom django.utils.decorators import method_decorator\nfrom django.views.decorators.http import require_POST\nfrom django.views import View\n\nfrom bookwyrm import book_search, forms, models\n\n# from bookwyrm.activitypub.base_activity import ActivityObject\nfrom bookwyrm.utils.isni import (\n    find_authors_by_name,\n    build_author_from_isni,\n    augment_author_metadata,\n)\nfrom bookwyrm.views.helpers import get_edition\nfrom .books import set_cover_from_url\n\n# pylint: disable=no-self-use\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass EditBook(View):\n    \"\"\"edit a book\"\"\"\n\n    def get(self, request, book_id):\n        \"\"\"info about a book\"\"\"\n        book = get_edition(book_id)\n        if not book.description:\n            book.description = book.parent_work.description\n        data = {\"book\": book, \"form\": forms.EditionForm(instance=book)}\n        return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n    def post(self, request, book_id):\n        \"\"\"edit a book cool\"\"\"\n        book = get_object_or_404(models.Edition, id=book_id)\n        form = forms.EditionForm(request.POST, request.FILES, instance=book)\n\n        data = {\"book\": book, \"form\": form}\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        data = add_authors(request, data)\n\n        # either of the above cases requires additional confirmation\n        if data.get(\"add_author\"):\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        remove_authors = request.POST.getlist(\"remove_authors\")\n        for author_id in remove_authors:\n            book.authors.remove(author_id)\n\n        book = form.save(commit=False)\n\n        url = request.POST.get(\"cover-url\")\n        if url:\n            image = set_cover_from_url(url)\n            if image:\n                book.cover.save(*image, save=False)\n\n        book.save()\n        return redirect(f\"/book/{book.id}\")\n\n\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass CreateBook(View):\n    \"\"\"brand new book\"\"\"\n\n    def get(self, request):\n        \"\"\"info about a book\"\"\"\n        data = {\"form\": forms.EditionForm()}\n        return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n    # pylint: disable=too-many-locals\n    def post(self, request):\n        \"\"\"create a new book\"\"\"\n        # returns None if no match is found\n        form = forms.EditionForm(request.POST, request.FILES)\n        data = {\"form\": form}\n\n        # collect data provided by the work or import item\n        parent_work_id = request.POST.get(\"parent_work\")\n        authors = None\n        if request.POST.get(\"authors\"):\n            author_ids = findall(r\"\\d+\", request.POST[\"authors\"])\n            authors = models.Author.objects.filter(id__in=author_ids)\n\n        # fake book in case we need to keep editing\n        if parent_work_id:\n            data[\"book\"] = {\n                \"parent_work\": {\"id\": parent_work_id},\n                \"authors\": authors,\n            }\n\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        data = add_authors(request, data)\n\n        # check if this is an edition of an existing work\n        author_text = \", \".join(data.get(\"add_author\", []))\n        data[\"book_matches\"] = book_search.search(\n            f'{form.cleaned_data.get(\"title\")} {author_text}',\n            min_confidence=0.1,\n        )[:5]\n\n        # go to confirm mode\n        if not parent_work_id or data.get(\"add_author\"):\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        with transaction.atomic():\n            book = form.save()\n            parent_work = get_object_or_404(models.Work, id=parent_work_id)\n            book.parent_work = parent_work\n\n            if authors:\n                book.authors.add(*authors)\n\n            url = request.POST.get(\"cover-url\")\n            if url:\n                image = set_cover_from_url(url)\n                if image:\n                    book.cover.save(*image, save=False)\n\n            book.save()\n        return redirect(f\"/book/{book.id}\")\n\n\ndef add_authors(request, data):\n    \"\"\"helper for adding authors\"\"\"\n    add_author = [author for author in request.POST.getlist(\"add_author\") if author]\n    if not add_author:\n        return data\n\n    data[\"add_author\"] = add_author\n    data[\"author_matches\"] = []\n    data[\"isni_matches\"] = []\n\n    # creting a book or adding an author to a book needs another step\n    data[\"confirm_mode\"] = True\n    # this isn't preserved because it isn't part of the form obj\n    data[\"remove_authors\"] = request.POST.getlist(\"remove_authors\")\n    data[\"cover_url\"] = request.POST.get(\"cover-url\")\n\n    for author in add_author:\n        # filter out empty author fields\n        if not author:\n            continue\n        # check for existing authors\n        vector = SearchVector(\"name\", weight=\"A\") + SearchVector(\"aliases\", weight=\"B\")\n\n        author_matches = (\n            models.Author.objects.annotate(search=vector)\n            .annotate(rank=SearchRank(vector, author))\n            .filter(rank__gt=0.4)\n            .order_by(\"-rank\")[:5]\n        )\n\n        isni_authors = find_authors_by_name(\n            author, description=True\n        )  # find matches from ISNI API\n\n        # dedupe isni authors we already have in the DB\n        exists = [\n            i\n            for i in isni_authors\n            for a in author_matches\n            if sub(r\"\\D\", \"\", str(i.isni)) == sub(r\"\\D\", \"\", str(a.isni))\n        ]\n\n        # pylint: disable=cell-var-from-loop\n        matches = list(filter(lambda x: x not in exists, isni_authors))\n        # combine existing and isni authors\n        matches.extend(author_matches)\n\n        data[\"author_matches\"].append(\n            {\n                \"name\": author.strip(),\n                \"matches\": matches,\n                \"existing_isnis\": exists,\n            }\n        )\n    return data\n\n\n@require_POST\n@permission_required(\"bookwyrm.edit_book\", raise_exception=True)\ndef create_book_from_data(request):\n    \"\"\"create a book with starter data\"\"\"\n    author_ids = findall(r\"\\d+\", request.POST.get(\"authors\"))\n    book = {\n        \"parent_work\": {\"id\": request.POST.get(\"parent_work\")},\n        \"authors\": models.Author.objects.filter(id__in=author_ids).all(),\n        \"subjects\": request.POST.getlist(\"subjects\"),\n    }\n\n    data = {\"book\": book, \"form\": forms.EditionForm(request.POST)}\n    return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n\n@method_decorator(login_required, name=\"dispatch\")\n@method_decorator(\n    permission_required(\"bookwyrm.edit_book\", raise_exception=True), name=\"dispatch\"\n)\nclass ConfirmEditBook(View):\n    \"\"\"confirm edits to a book\"\"\"\n\n    # pylint: disable=too-many-locals\n    # pylint: disable=too-many-branches\n    def post(self, request, book_id=None):\n        \"\"\"edit a book cool\"\"\"\n        # returns None if no match is found\n        book = models.Edition.objects.filter(id=book_id).first()\n        form = forms.EditionForm(request.POST, request.FILES, instance=book)\n\n        data = {\"book\": book, \"form\": form}\n        if not form.is_valid():\n            return TemplateResponse(request, \"book/edit/edit_book.html\", data)\n\n        with transaction.atomic():\n            # save book\n            book = form.save()\n\n            # add known authors\n            authors = None\n            if request.POST.get(\"authors\"):\n                author_ids = findall(r\"\\d+\", request.POST[\"authors\"])\n                authors = models.Author.objects.filter(id__in=author_ids)\n                book.authors.add(*authors)\n\n            # get or create author as needed\n            for i in range(int(request.POST.get(\"author-match-count\", 0))):\n                match = request.POST.get(f\"author_match-{i}\")\n                if not match:\n                    return HttpResponseBadRequest()\n                try:\n                    # if it's an int, it's an ID\n                    match = int(match)\n                    author = get_object_or_404(\n                        models.Author, id=request.POST[f\"author_match-{i}\"]\n                    )\n                    # update author metadata if the ISNI record is more complete\n                    isni = request.POST.get(f\"isni-for-{match}\", None)\n                    if isni is not None:\n                        augment_author_metadata(author, isni)\n                except ValueError:\n                    # otherwise it's a new author\n                    isni_match = request.POST.get(f\"author_match-{i}\")\n                    author_object = build_author_from_isni(isni_match)\n                    # with author data class from isni id\n                    if \"author\" in author_object:\n                        skeleton = models.Author.objects.create(\n                            name=author_object[\"author\"].name\n                        )\n                        author = author_object[\"author\"].to_model(\n                            model=models.Author, overwrite=True, instance=skeleton\n                        )\n                    else:\n                        # or it's just a name\n                        author = models.Author.objects.create(name=match)\n                book.authors.add(author)\n\n            # create work, if needed\n            if not book.parent_work:\n                work_match = request.POST.get(\"parent_work\")\n                if work_match and work_match != \"0\":\n                    work = get_object_or_404(models.Work, id=work_match)\n                else:\n                    work = models.Work.objects.create(title=form.cleaned_data[\"title\"])\n                    work.authors.set(book.authors.all())\n                book.parent_work = work\n\n            for author_id in request.POST.getlist(\"remove_authors\"):\n                book.authors.remove(author_id)\n\n            # import cover, if requested\n            url = request.POST.get(\"cover-url\")\n            if url:\n                image = set_cover_from_url(url)\n                if image:\n                    book.cover.save(*image, save=False)\n\n            # we don't tell the world when creating a book\n            book.save(broadcast=False)\n\n        return redirect(f\"/book/{book.id}\")\n", "path": "bookwyrm/views/books/edit_book.py"}]}