We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add publisher as a book field
Currently, the only way to distinguish editions is by their cover and their publication date. It would be nice if editions were also referred to by their publisher.

This is especially useful as part of the alt text of a cover for screen reader users, but also great for people who would like to catalog which edition is by which publisher. I believe OpenLibrary provides a publisher field as well, so this could be automagically filled during an import.

--- 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/models/book.py`
Content:
```
1 """ database schema for books and shelves """
2 import re
3 
4 from django.db import models
5 from model_utils.managers import InheritanceManager
6 
7 from bookwyrm import activitypub
8 from bookwyrm.settings import DOMAIN
9 
10 from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
11 from .base_model import BookWyrmModel
12 from . import fields
13 
14 
15 class BookDataModel(ObjectMixin, BookWyrmModel):
16     """ fields shared between editable book data (books, works, authors) """
17 
18     origin_id = models.CharField(max_length=255, null=True, blank=True)
19     openlibrary_key = fields.CharField(
20         max_length=255, blank=True, null=True, deduplication_field=True
21     )
22     librarything_key = fields.CharField(
23         max_length=255, blank=True, null=True, deduplication_field=True
24     )
25     goodreads_key = fields.CharField(
26         max_length=255, blank=True, null=True, deduplication_field=True
27     )
28 
29     last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True)
30 
31     class Meta:
32         """ can't initialize this model, that wouldn't make sense """
33 
34         abstract = True
35 
36     def save(self, *args, **kwargs):
37         """ ensure that the remote_id is within this instance """
38         if self.id:
39             self.remote_id = self.get_remote_id()
40         else:
41             self.origin_id = self.remote_id
42             self.remote_id = None
43         return super().save(*args, **kwargs)
44 
45     def broadcast(self, activity, sender, software="bookwyrm"):
46         """ only send book data updates to other bookwyrm instances """
47         super().broadcast(activity, sender, software=software)
48 
49 
50 class Book(BookDataModel):
51     """ a generic book, which can mean either an edition or a work """
52 
53     connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
54 
55     # book/work metadata
56     title = fields.CharField(max_length=255)
57     sort_title = fields.CharField(max_length=255, blank=True, null=True)
58     subtitle = fields.CharField(max_length=255, blank=True, null=True)
59     description = fields.HtmlField(blank=True, null=True)
60     languages = fields.ArrayField(
61         models.CharField(max_length=255), blank=True, default=list
62     )
63     series = fields.CharField(max_length=255, blank=True, null=True)
64     series_number = fields.CharField(max_length=255, blank=True, null=True)
65     subjects = fields.ArrayField(
66         models.CharField(max_length=255), blank=True, null=True, default=list
67     )
68     subject_places = fields.ArrayField(
69         models.CharField(max_length=255), blank=True, null=True, default=list
70     )
71     authors = fields.ManyToManyField("Author")
72     cover = fields.ImageField(
73         upload_to="covers/", blank=True, null=True, alt_field="alt_text"
74     )
75     first_published_date = fields.DateTimeField(blank=True, null=True)
76     published_date = fields.DateTimeField(blank=True, null=True)
77 
78     objects = InheritanceManager()
79 
80     @property
81     def author_text(self):
82         """ format a list of authors """
83         return ", ".join(a.name for a in self.authors.all())
84 
85     @property
86     def latest_readthrough(self):
87         """ most recent readthrough activity """
88         return self.readthrough_set.order_by("-updated_date").first()
89 
90     @property
91     def edition_info(self):
92         """ properties of this edition, as a string """
93         items = [
94             self.physical_format if hasattr(self, "physical_format") else None,
95             self.languages[0] + " language"
96             if self.languages and self.languages[0] != "English"
97             else None,
98             str(self.published_date.year) if self.published_date else None,
99         ]
100         return ", ".join(i for i in items if i)
101 
102     @property
103     def alt_text(self):
104         """ image alt test """
105         text = "%s" % self.title
106         if self.edition_info:
107             text += " (%s)" % self.edition_info
108         return text
109 
110     def save(self, *args, **kwargs):
111         """ can't be abstract for query reasons, but you shouldn't USE it """
112         if not isinstance(self, Edition) and not isinstance(self, Work):
113             raise ValueError("Books should be added as Editions or Works")
114         return super().save(*args, **kwargs)
115 
116     def get_remote_id(self):
117         """ editions and works both use "book" instead of model_name """
118         return "https://%s/book/%d" % (DOMAIN, self.id)
119 
120     def __repr__(self):
121         return "<{} key={!r} title={!r}>".format(
122             self.__class__,
123             self.openlibrary_key,
124             self.title,
125         )
126 
127 
128 class Work(OrderedCollectionPageMixin, Book):
129     """ a work (an abstract concept of a book that manifests in an edition) """
130 
131     # library of congress catalog control number
132     lccn = fields.CharField(
133         max_length=255, blank=True, null=True, deduplication_field=True
134     )
135     # this has to be nullable but should never be null
136     default_edition = fields.ForeignKey(
137         "Edition", on_delete=models.PROTECT, null=True, load_remote=False
138     )
139 
140     def save(self, *args, **kwargs):
141         """ set some fields on the edition object """
142         # set rank
143         for edition in self.editions.all():
144             edition.save()
145         return super().save(*args, **kwargs)
146 
147     def get_default_edition(self):
148         """ in case the default edition is not set """
149         return self.default_edition or self.editions.order_by("-edition_rank").first()
150 
151     def to_edition_list(self, **kwargs):
152         """ an ordered collection of editions """
153         return self.to_ordered_collection(
154             self.editions.order_by("-edition_rank").all(),
155             remote_id="%s/editions" % self.remote_id,
156             **kwargs
157         )
158 
159     activity_serializer = activitypub.Work
160     serialize_reverse_fields = [("editions", "editions", "-edition_rank")]
161     deserialize_reverse_fields = [("editions", "editions")]
162 
163 
164 class Edition(Book):
165     """ an edition of a book """
166 
167     # these identifiers only apply to editions, not works
168     isbn_10 = fields.CharField(
169         max_length=255, blank=True, null=True, deduplication_field=True
170     )
171     isbn_13 = fields.CharField(
172         max_length=255, blank=True, null=True, deduplication_field=True
173     )
174     oclc_number = fields.CharField(
175         max_length=255, blank=True, null=True, deduplication_field=True
176     )
177     asin = fields.CharField(
178         max_length=255, blank=True, null=True, deduplication_field=True
179     )
180     pages = fields.IntegerField(blank=True, null=True)
181     physical_format = fields.CharField(max_length=255, blank=True, null=True)
182     publishers = fields.ArrayField(
183         models.CharField(max_length=255), blank=True, default=list
184     )
185     shelves = models.ManyToManyField(
186         "Shelf",
187         symmetrical=False,
188         through="ShelfBook",
189         through_fields=("book", "shelf"),
190     )
191     parent_work = fields.ForeignKey(
192         "Work",
193         on_delete=models.PROTECT,
194         null=True,
195         related_name="editions",
196         activitypub_field="work",
197     )
198     edition_rank = fields.IntegerField(default=0)
199 
200     activity_serializer = activitypub.Edition
201     name_field = "title"
202 
203     def get_rank(self):
204         """ calculate how complete the data is on this edition """
205         if self.parent_work and self.parent_work.default_edition == self:
206             # default edition has the highest rank
207             return 20
208         rank = 0
209         rank += int(bool(self.cover)) * 3
210         rank += int(bool(self.isbn_13))
211         rank += int(bool(self.isbn_10))
212         rank += int(bool(self.oclc_number))
213         rank += int(bool(self.pages))
214         rank += int(bool(self.physical_format))
215         rank += int(bool(self.description))
216         # max rank is 9
217         return rank
218 
219     def save(self, *args, **kwargs):
220         """ set some fields on the edition object """
221         # calculate isbn 10/13
222         if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10:
223             self.isbn_10 = isbn_13_to_10(self.isbn_13)
224         if self.isbn_10 and not self.isbn_13:
225             self.isbn_13 = isbn_10_to_13(self.isbn_10)
226 
227         # set rank
228         self.edition_rank = self.get_rank()
229 
230         return super().save(*args, **kwargs)
231 
232 
233 def isbn_10_to_13(isbn_10):
234     """ convert an isbn 10 into an isbn 13 """
235     isbn_10 = re.sub(r"[^0-9X]", "", isbn_10)
236     # drop the last character of the isbn 10 number (the original checkdigit)
237     converted = isbn_10[:9]
238     # add "978" to the front
239     converted = "978" + converted
240     # add a check digit to the end
241     # multiply the odd digits by 1 and the even digits by 3 and sum them
242     try:
243         checksum = sum(int(i) for i in converted[::2]) + sum(
244             int(i) * 3 for i in converted[1::2]
245         )
246     except ValueError:
247         return None
248     # add the checksum mod 10 to the end
249     checkdigit = checksum % 10
250     if checkdigit != 0:
251         checkdigit = 10 - checkdigit
252     return converted + str(checkdigit)
253 
254 
255 def isbn_13_to_10(isbn_13):
256     """ convert isbn 13 to 10, if possible """
257     if isbn_13[:3] != "978":
258         return None
259 
260     isbn_13 = re.sub(r"[^0-9X]", "", isbn_13)
261 
262     # remove '978' and old checkdigit
263     converted = isbn_13[3:-1]
264     # calculate checkdigit
265     # multiple each digit by 10,9,8.. successively and sum them
266     try:
267         checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted))
268     except ValueError:
269         return None
270     checkdigit = checksum % 11
271     checkdigit = 11 - checkdigit
272     if checkdigit == 10:
273         checkdigit = "X"
274     return converted + str(checkdigit)
275
```
--- 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/models/book.py b/bookwyrm/models/book.py
--- a/bookwyrm/models/book.py
+++ b/bookwyrm/models/book.py
@@ -96,6 +96,7 @@
             if self.languages and self.languages[0] != "English"
             else None,
             str(self.published_date.year) if self.published_date else None,
+            ", ".join(self.publishers) if hasattr(self, "publishers") else None,
         ]
         return ", ".join(i for i in items if i)

{"golden_diff": "diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py\n--- a/bookwyrm/models/book.py\n+++ b/bookwyrm/models/book.py\n@@ -96,6 +96,7 @@\n             if self.languages and self.languages[0] != \"English\"\n             else None,\n             str(self.published_date.year) if self.published_date else None,\n+            \", \".join(self.publishers) if hasattr(self, \"publishers\") else None,\n         ]\n         return \", \".join(i for i in items if i)\n", "issue": "Add publisher as a book field\nCurrently, the only way to distinguish editions is by their cover and their publication date. It would be nice if editions were also referred to by their publisher.\r\n\r\nThis is especially useful as part of the alt text of a cover for screen reader users, but also great for people who would like to catalog which edition is by which publisher. I believe OpenLibrary provides a publisher field as well, so this could be automagically filled during an import.\n", "before_files": [{"content": "\"\"\" database schema for books and shelves \"\"\"\nimport re\n\nfrom django.db import models\nfrom model_utils.managers import InheritanceManager\n\nfrom bookwyrm import activitypub\nfrom bookwyrm.settings import DOMAIN\n\nfrom .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin\nfrom .base_model import BookWyrmModel\nfrom . import fields\n\n\nclass BookDataModel(ObjectMixin, BookWyrmModel):\n    \"\"\" fields shared between editable book data (books, works, authors) \"\"\"\n\n    origin_id = models.CharField(max_length=255, null=True, blank=True)\n    openlibrary_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    librarything_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    goodreads_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n\n    last_edited_by = models.ForeignKey(\"User\", on_delete=models.PROTECT, null=True)\n\n    class Meta:\n        \"\"\" can't initialize this model, that wouldn't make sense \"\"\"\n\n        abstract = True\n\n    def save(self, *args, **kwargs):\n        \"\"\" ensure that the remote_id is within this instance \"\"\"\n        if self.id:\n            self.remote_id = self.get_remote_id()\n        else:\n            self.origin_id = self.remote_id\n            self.remote_id = None\n        return super().save(*args, **kwargs)\n\n    def broadcast(self, activity, sender, software=\"bookwyrm\"):\n        \"\"\" only send book data updates to other bookwyrm instances \"\"\"\n        super().broadcast(activity, sender, software=software)\n\n\nclass Book(BookDataModel):\n    \"\"\" a generic book, which can mean either an edition or a work \"\"\"\n\n    connector = models.ForeignKey(\"Connector\", on_delete=models.PROTECT, null=True)\n\n    # book/work metadata\n    title = fields.CharField(max_length=255)\n    sort_title = fields.CharField(max_length=255, blank=True, null=True)\n    subtitle = fields.CharField(max_length=255, blank=True, null=True)\n    description = fields.HtmlField(blank=True, null=True)\n    languages = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, default=list\n    )\n    series = fields.CharField(max_length=255, blank=True, null=True)\n    series_number = fields.CharField(max_length=255, blank=True, null=True)\n    subjects = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, null=True, default=list\n    )\n    subject_places = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, null=True, default=list\n    )\n    authors = fields.ManyToManyField(\"Author\")\n    cover = fields.ImageField(\n        upload_to=\"covers/\", blank=True, null=True, alt_field=\"alt_text\"\n    )\n    first_published_date = fields.DateTimeField(blank=True, null=True)\n    published_date = fields.DateTimeField(blank=True, null=True)\n\n    objects = InheritanceManager()\n\n    @property\n    def author_text(self):\n        \"\"\" format a list of authors \"\"\"\n        return \", \".join(a.name for a in self.authors.all())\n\n    @property\n    def latest_readthrough(self):\n        \"\"\" most recent readthrough activity \"\"\"\n        return self.readthrough_set.order_by(\"-updated_date\").first()\n\n    @property\n    def edition_info(self):\n        \"\"\" properties of this edition, as a string \"\"\"\n        items = [\n            self.physical_format if hasattr(self, \"physical_format\") else None,\n            self.languages[0] + \" language\"\n            if self.languages and self.languages[0] != \"English\"\n            else None,\n            str(self.published_date.year) if self.published_date else None,\n        ]\n        return \", \".join(i for i in items if i)\n\n    @property\n    def alt_text(self):\n        \"\"\" image alt test \"\"\"\n        text = \"%s\" % self.title\n        if self.edition_info:\n            text += \" (%s)\" % self.edition_info\n        return text\n\n    def save(self, *args, **kwargs):\n        \"\"\" can't be abstract for query reasons, but you shouldn't USE it \"\"\"\n        if not isinstance(self, Edition) and not isinstance(self, Work):\n            raise ValueError(\"Books should be added as Editions or Works\")\n        return super().save(*args, **kwargs)\n\n    def get_remote_id(self):\n        \"\"\" editions and works both use \"book\" instead of model_name \"\"\"\n        return \"https://%s/book/%d\" % (DOMAIN, self.id)\n\n    def __repr__(self):\n        return \"<{} key={!r} title={!r}>\".format(\n            self.__class__,\n            self.openlibrary_key,\n            self.title,\n        )\n\n\nclass Work(OrderedCollectionPageMixin, Book):\n    \"\"\" a work (an abstract concept of a book that manifests in an edition) \"\"\"\n\n    # library of congress catalog control number\n    lccn = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    # this has to be nullable but should never be null\n    default_edition = fields.ForeignKey(\n        \"Edition\", on_delete=models.PROTECT, null=True, load_remote=False\n    )\n\n    def save(self, *args, **kwargs):\n        \"\"\" set some fields on the edition object \"\"\"\n        # set rank\n        for edition in self.editions.all():\n            edition.save()\n        return super().save(*args, **kwargs)\n\n    def get_default_edition(self):\n        \"\"\" in case the default edition is not set \"\"\"\n        return self.default_edition or self.editions.order_by(\"-edition_rank\").first()\n\n    def to_edition_list(self, **kwargs):\n        \"\"\" an ordered collection of editions \"\"\"\n        return self.to_ordered_collection(\n            self.editions.order_by(\"-edition_rank\").all(),\n            remote_id=\"%s/editions\" % self.remote_id,\n            **kwargs\n        )\n\n    activity_serializer = activitypub.Work\n    serialize_reverse_fields = [(\"editions\", \"editions\", \"-edition_rank\")]\n    deserialize_reverse_fields = [(\"editions\", \"editions\")]\n\n\nclass Edition(Book):\n    \"\"\" an edition of a book \"\"\"\n\n    # these identifiers only apply to editions, not works\n    isbn_10 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    isbn_13 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    oclc_number = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    asin = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    pages = fields.IntegerField(blank=True, null=True)\n    physical_format = fields.CharField(max_length=255, blank=True, null=True)\n    publishers = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, default=list\n    )\n    shelves = models.ManyToManyField(\n        \"Shelf\",\n        symmetrical=False,\n        through=\"ShelfBook\",\n        through_fields=(\"book\", \"shelf\"),\n    )\n    parent_work = fields.ForeignKey(\n        \"Work\",\n        on_delete=models.PROTECT,\n        null=True,\n        related_name=\"editions\",\n        activitypub_field=\"work\",\n    )\n    edition_rank = fields.IntegerField(default=0)\n\n    activity_serializer = activitypub.Edition\n    name_field = \"title\"\n\n    def get_rank(self):\n        \"\"\" calculate how complete the data is on this edition \"\"\"\n        if self.parent_work and self.parent_work.default_edition == self:\n            # default edition has the highest rank\n            return 20\n        rank = 0\n        rank += int(bool(self.cover)) * 3\n        rank += int(bool(self.isbn_13))\n        rank += int(bool(self.isbn_10))\n        rank += int(bool(self.oclc_number))\n        rank += int(bool(self.pages))\n        rank += int(bool(self.physical_format))\n        rank += int(bool(self.description))\n        # max rank is 9\n        return rank\n\n    def save(self, *args, **kwargs):\n        \"\"\" set some fields on the edition object \"\"\"\n        # calculate isbn 10/13\n        if self.isbn_13 and self.isbn_13[:3] == \"978\" and not self.isbn_10:\n            self.isbn_10 = isbn_13_to_10(self.isbn_13)\n        if self.isbn_10 and not self.isbn_13:\n            self.isbn_13 = isbn_10_to_13(self.isbn_10)\n\n        # set rank\n        self.edition_rank = self.get_rank()\n\n        return super().save(*args, **kwargs)\n\n\ndef isbn_10_to_13(isbn_10):\n    \"\"\" convert an isbn 10 into an isbn 13 \"\"\"\n    isbn_10 = re.sub(r\"[^0-9X]\", \"\", isbn_10)\n    # drop the last character of the isbn 10 number (the original checkdigit)\n    converted = isbn_10[:9]\n    # add \"978\" to the front\n    converted = \"978\" + converted\n    # add a check digit to the end\n    # multiply the odd digits by 1 and the even digits by 3 and sum them\n    try:\n        checksum = sum(int(i) for i in converted[::2]) + sum(\n            int(i) * 3 for i in converted[1::2]\n        )\n    except ValueError:\n        return None\n    # add the checksum mod 10 to the end\n    checkdigit = checksum % 10\n    if checkdigit != 0:\n        checkdigit = 10 - checkdigit\n    return converted + str(checkdigit)\n\n\ndef isbn_13_to_10(isbn_13):\n    \"\"\" convert isbn 13 to 10, if possible \"\"\"\n    if isbn_13[:3] != \"978\":\n        return None\n\n    isbn_13 = re.sub(r\"[^0-9X]\", \"\", isbn_13)\n\n    # remove '978' and old checkdigit\n    converted = isbn_13[3:-1]\n    # calculate checkdigit\n    # multiple each digit by 10,9,8.. successively and sum them\n    try:\n        checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted))\n    except ValueError:\n        return None\n    checkdigit = checksum % 11\n    checkdigit = 11 - checkdigit\n    if checkdigit == 10:\n        checkdigit = \"X\"\n    return converted + str(checkdigit)\n", "path": "bookwyrm/models/book.py"}], "after_files": [{"content": "\"\"\" database schema for books and shelves \"\"\"\nimport re\n\nfrom django.db import models\nfrom model_utils.managers import InheritanceManager\n\nfrom bookwyrm import activitypub\nfrom bookwyrm.settings import DOMAIN\n\nfrom .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin\nfrom .base_model import BookWyrmModel\nfrom . import fields\n\n\nclass BookDataModel(ObjectMixin, BookWyrmModel):\n    \"\"\" fields shared between editable book data (books, works, authors) \"\"\"\n\n    origin_id = models.CharField(max_length=255, null=True, blank=True)\n    openlibrary_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    librarything_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    goodreads_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n\n    last_edited_by = models.ForeignKey(\"User\", on_delete=models.PROTECT, null=True)\n\n    class Meta:\n        \"\"\" can't initialize this model, that wouldn't make sense \"\"\"\n\n        abstract = True\n\n    def save(self, *args, **kwargs):\n        \"\"\" ensure that the remote_id is within this instance \"\"\"\n        if self.id:\n            self.remote_id = self.get_remote_id()\n        else:\n            self.origin_id = self.remote_id\n            self.remote_id = None\n        return super().save(*args, **kwargs)\n\n    def broadcast(self, activity, sender, software=\"bookwyrm\"):\n        \"\"\" only send book data updates to other bookwyrm instances \"\"\"\n        super().broadcast(activity, sender, software=software)\n\n\nclass Book(BookDataModel):\n    \"\"\" a generic book, which can mean either an edition or a work \"\"\"\n\n    connector = models.ForeignKey(\"Connector\", on_delete=models.PROTECT, null=True)\n\n    # book/work metadata\n    title = fields.CharField(max_length=255)\n    sort_title = fields.CharField(max_length=255, blank=True, null=True)\n    subtitle = fields.CharField(max_length=255, blank=True, null=True)\n    description = fields.HtmlField(blank=True, null=True)\n    languages = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, default=list\n    )\n    series = fields.CharField(max_length=255, blank=True, null=True)\n    series_number = fields.CharField(max_length=255, blank=True, null=True)\n    subjects = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, null=True, default=list\n    )\n    subject_places = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, null=True, default=list\n    )\n    authors = fields.ManyToManyField(\"Author\")\n    cover = fields.ImageField(\n        upload_to=\"covers/\", blank=True, null=True, alt_field=\"alt_text\"\n    )\n    first_published_date = fields.DateTimeField(blank=True, null=True)\n    published_date = fields.DateTimeField(blank=True, null=True)\n\n    objects = InheritanceManager()\n\n    @property\n    def author_text(self):\n        \"\"\" format a list of authors \"\"\"\n        return \", \".join(a.name for a in self.authors.all())\n\n    @property\n    def latest_readthrough(self):\n        \"\"\" most recent readthrough activity \"\"\"\n        return self.readthrough_set.order_by(\"-updated_date\").first()\n\n    @property\n    def edition_info(self):\n        \"\"\" properties of this edition, as a string \"\"\"\n        items = [\n            self.physical_format if hasattr(self, \"physical_format\") else None,\n            self.languages[0] + \" language\"\n            if self.languages and self.languages[0] != \"English\"\n            else None,\n            str(self.published_date.year) if self.published_date else None,\n            \", \".join(self.publishers) if hasattr(self, \"publishers\") else None,\n        ]\n        return \", \".join(i for i in items if i)\n\n    @property\n    def alt_text(self):\n        \"\"\" image alt test \"\"\"\n        text = \"%s\" % self.title\n        if self.edition_info:\n            text += \" (%s)\" % self.edition_info\n        return text\n\n    def save(self, *args, **kwargs):\n        \"\"\" can't be abstract for query reasons, but you shouldn't USE it \"\"\"\n        if not isinstance(self, Edition) and not isinstance(self, Work):\n            raise ValueError(\"Books should be added as Editions or Works\")\n        return super().save(*args, **kwargs)\n\n    def get_remote_id(self):\n        \"\"\" editions and works both use \"book\" instead of model_name \"\"\"\n        return \"https://%s/book/%d\" % (DOMAIN, self.id)\n\n    def __repr__(self):\n        return \"<{} key={!r} title={!r}>\".format(\n            self.__class__,\n            self.openlibrary_key,\n            self.title,\n        )\n\n\nclass Work(OrderedCollectionPageMixin, Book):\n    \"\"\" a work (an abstract concept of a book that manifests in an edition) \"\"\"\n\n    # library of congress catalog control number\n    lccn = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    # this has to be nullable but should never be null\n    default_edition = fields.ForeignKey(\n        \"Edition\", on_delete=models.PROTECT, null=True, load_remote=False\n    )\n\n    def save(self, *args, **kwargs):\n        \"\"\" set some fields on the edition object \"\"\"\n        # set rank\n        for edition in self.editions.all():\n            edition.save()\n        return super().save(*args, **kwargs)\n\n    def get_default_edition(self):\n        \"\"\" in case the default edition is not set \"\"\"\n        return self.default_edition or self.editions.order_by(\"-edition_rank\").first()\n\n    def to_edition_list(self, **kwargs):\n        \"\"\" an ordered collection of editions \"\"\"\n        return self.to_ordered_collection(\n            self.editions.order_by(\"-edition_rank\").all(),\n            remote_id=\"%s/editions\" % self.remote_id,\n            **kwargs\n        )\n\n    activity_serializer = activitypub.Work\n    serialize_reverse_fields = [(\"editions\", \"editions\", \"-edition_rank\")]\n    deserialize_reverse_fields = [(\"editions\", \"editions\")]\n\n\nclass Edition(Book):\n    \"\"\" an edition of a book \"\"\"\n\n    # these identifiers only apply to editions, not works\n    isbn_10 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    isbn_13 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    oclc_number = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    asin = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True\n    )\n    pages = fields.IntegerField(blank=True, null=True)\n    physical_format = fields.CharField(max_length=255, blank=True, null=True)\n    publishers = fields.ArrayField(\n        models.CharField(max_length=255), blank=True, default=list\n    )\n    shelves = models.ManyToManyField(\n        \"Shelf\",\n        symmetrical=False,\n        through=\"ShelfBook\",\n        through_fields=(\"book\", \"shelf\"),\n    )\n    parent_work = fields.ForeignKey(\n        \"Work\",\n        on_delete=models.PROTECT,\n        null=True,\n        related_name=\"editions\",\n        activitypub_field=\"work\",\n    )\n    edition_rank = fields.IntegerField(default=0)\n\n    activity_serializer = activitypub.Edition\n    name_field = \"title\"\n\n    def get_rank(self):\n        \"\"\" calculate how complete the data is on this edition \"\"\"\n        if self.parent_work and self.parent_work.default_edition == self:\n            # default edition has the highest rank\n            return 20\n        rank = 0\n        rank += int(bool(self.cover)) * 3\n        rank += int(bool(self.isbn_13))\n        rank += int(bool(self.isbn_10))\n        rank += int(bool(self.oclc_number))\n        rank += int(bool(self.pages))\n        rank += int(bool(self.physical_format))\n        rank += int(bool(self.description))\n        # max rank is 9\n        return rank\n\n    def save(self, *args, **kwargs):\n        \"\"\" set some fields on the edition object \"\"\"\n        # calculate isbn 10/13\n        if self.isbn_13 and self.isbn_13[:3] == \"978\" and not self.isbn_10:\n            self.isbn_10 = isbn_13_to_10(self.isbn_13)\n        if self.isbn_10 and not self.isbn_13:\n            self.isbn_13 = isbn_10_to_13(self.isbn_10)\n\n        # set rank\n        self.edition_rank = self.get_rank()\n\n        return super().save(*args, **kwargs)\n\n\ndef isbn_10_to_13(isbn_10):\n    \"\"\" convert an isbn 10 into an isbn 13 \"\"\"\n    isbn_10 = re.sub(r\"[^0-9X]\", \"\", isbn_10)\n    # drop the last character of the isbn 10 number (the original checkdigit)\n    converted = isbn_10[:9]\n    # add \"978\" to the front\n    converted = \"978\" + converted\n    # add a check digit to the end\n    # multiply the odd digits by 1 and the even digits by 3 and sum them\n    try:\n        checksum = sum(int(i) for i in converted[::2]) + sum(\n            int(i) * 3 for i in converted[1::2]\n        )\n    except ValueError:\n        return None\n    # add the checksum mod 10 to the end\n    checkdigit = checksum % 10\n    if checkdigit != 0:\n        checkdigit = 10 - checkdigit\n    return converted + str(checkdigit)\n\n\ndef isbn_13_to_10(isbn_13):\n    \"\"\" convert isbn 13 to 10, if possible \"\"\"\n    if isbn_13[:3] != \"978\":\n        return None\n\n    isbn_13 = re.sub(r\"[^0-9X]\", \"\", isbn_13)\n\n    # remove '978' and old checkdigit\n    converted = isbn_13[3:-1]\n    # calculate checkdigit\n    # multiple each digit by 10,9,8.. successively and sum them\n    try:\n        checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted))\n    except ValueError:\n        return None\n    checkdigit = checksum % 11\n    checkdigit = 11 - checkdigit\n    if checkdigit == 10:\n        checkdigit = \"X\"\n    return converted + str(checkdigit)\n", "path": "bookwyrm/models/book.py"}]}