We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Remove the "Cover" text from the alt text of book covers if one is present
Currently, when a book cover is present and is displayed, it's alt text consists of the book title, the text *Cover*, edition name, and the first published date.

For example, via VoiceOver under Safari:

```
image The Night Circus cover (Hardcover, 2011)
```

The fact that this is a cover image is redundant, because users are already notified about the presence of an image. In this case, the textual data is actually more important, e.g. book title and edition name, since the presence of a cover does not add more information when using a screen reader.

The expected result, via VoiceOver (and possibly other screen readers as well) is:

```
image The Night Circus (Hardcover, 2011)
```

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

{"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@@ -91,7 +91,7 @@\n     @property\n     def alt_text(self):\n         ''' image alt test '''\n-        text = '%s cover' % self.title\n+        text = '%s' % self.title\n         if self.edition_info:\n             text += ' (%s)' % self.edition_info\n         return text\n", "issue": "Remove the \"Cover\" text from the alt text of book covers if one is present\nCurrently, when a book cover is present and is displayed, it's alt text consists of the book title, the text *Cover*, edition name, and the first published date.\r\n\r\nFor example, via VoiceOver under Safari:\r\n\r\n```\r\nimage The Night Circus cover (Hardcover, 2011)\r\n```\r\n\r\nThe fact that this is a cover image is redundant, because users are already notified about the presence of an image. In this case, the textual data is actually more important, e.g. book title and edition name, since the presence of a cover does not add more information when using a screen reader.\r\n\r\nThe expected result, via VoiceOver (and possibly other screen readers as well) is:\r\n\r\n```\r\nimage The Night Circus (Hardcover, 2011)\r\n```\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\nclass BookDataModel(ObjectMixin, BookWyrmModel):\n    ''' fields shared between editable book data (books, works, authors) '''\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    librarything_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    goodreads_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n\n    last_edited_by = models.ForeignKey(\n        'User', on_delete=models.PROTECT, null=True)\n\n    class Meta:\n        ''' can't initialize this model, that wouldn't make sense '''\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\nclass Book(BookDataModel):\n    ''' a generic book, which can mean either an edition or a work '''\n    connector = models.ForeignKey(\n        '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    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' if self.languages and \\\n                    self.languages[0] != 'English' 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 cover' % 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    # library of congress catalog control number\n    lccn = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    # this has to be nullable but should never be null\n    default_edition = fields.ForeignKey(\n        'Edition',\n        on_delete=models.PROTECT,\n        null=True,\n        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(\n            '-edition_rank'\n        ).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    # 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    isbn_13 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    oclc_number = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    asin = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\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', on_delete=models.PROTECT, null=True,\n        related_name='editions', activitypub_field='work')\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]) + \\\n               sum(int(i) * 3 for i in converted[1::2])\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\nclass BookDataModel(ObjectMixin, BookWyrmModel):\n    ''' fields shared between editable book data (books, works, authors) '''\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    librarything_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    goodreads_key = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n\n    last_edited_by = models.ForeignKey(\n        'User', on_delete=models.PROTECT, null=True)\n\n    class Meta:\n        ''' can't initialize this model, that wouldn't make sense '''\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\nclass Book(BookDataModel):\n    ''' a generic book, which can mean either an edition or a work '''\n    connector = models.ForeignKey(\n        '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    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' if self.languages and \\\n                    self.languages[0] != 'English' 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    # library of congress catalog control number\n    lccn = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    # this has to be nullable but should never be null\n    default_edition = fields.ForeignKey(\n        'Edition',\n        on_delete=models.PROTECT,\n        null=True,\n        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(\n            '-edition_rank'\n        ).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    # 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    isbn_13 = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    oclc_number = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\n    asin = fields.CharField(\n        max_length=255, blank=True, null=True, deduplication_field=True)\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', on_delete=models.PROTECT, null=True,\n        related_name='editions', activitypub_field='work')\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]) + \\\n               sum(int(i) * 3 for i in converted[1::2])\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"}]}