Repository: internetarchive/openlibrary

Issue:
Orphaned editions error
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->

```
/opt/openlibrary/deploys/openlibrary/9afb5a2/openlibrary/templates/type/edition/view.html: error in processing template: IndexError: list index out of range (falling back to default template)
```

### Relevant url?
- https://openlibrary.org/books/OL9773440M/Muertos_Incomodos?v=1
- https://openlibrary.org/books/OL9773440M/Muertos_Incomodos?v=1&debug=true

### Details

- **Logged in (Y/N)?** Y
- **Browser type/version?** FF79
- **Operating system?** Win10
- **Environment (prod/dev/local)?** prod
<!-- If not sure, put prod -->

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->

### Related files
<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->

### Stakeholders
@mekarpeles 



Discussion:
Dupe of #4051

Resolving patch:
diff --git a/openlibrary/core/models.py b/openlibrary/core/models.py
--- a/openlibrary/core/models.py
+++ b/openlibrary/core/models.py
@@ -415,6 +415,21 @@ def is_ia_scan(self):
         # all IA scans will have scanningcenter field set
         return bool(metadata.get("scanningcenter"))
 
+    def make_work_from_orphaned_edition(self):
+        """
+        Create a dummy work from an orphaned_edition.
+        """
+        return web.ctx.site.new('', {
+            'key': '',
+            'type': {'key': '/type/work'},
+            'title': self.title,
+            'authors': [
+                {'type': {'key': '/type/author_role'}, 'author': {'key': a['key']}}
+                for a in self.get('authors', [])],
+            'editions': [self],
+            'subjects': self.get('subjects', []),
+        })
+
 def some(values):
     """Returns the first value that is True from the values iterator.
     Works like any, but returns the value instead of bool(value).
@@ -471,6 +486,8 @@ def get_users_read_status(self, username):
         return status_id
 
     def get_num_users_by_bookshelf(self):
+        if not self.key:  # a dummy work
+            return {'want-to-read': 0, 'currently-reading': 0, 'already-read': 0}
         work_id = extract_numeric_id_from_olid(self.key)
         num_users_by_bookshelf = Bookshelves.get_num_users_by_bookshelf_by_work_id(work_id)
         return {
@@ -480,12 +497,14 @@ def get_num_users_by_bookshelf(self):
         }
 
     def get_rating_stats(self):
+        if not self.key:  # a dummy work
+            return {'avg_rating': 0, 'num_ratings': 0}
         work_id = extract_numeric_id_from_olid(self.key)
         rating_stats = Ratings.get_rating_stats(work_id)
         if rating_stats and rating_stats['num_ratings'] > 0:
             return {
-            'avg_rating': round(rating_stats['avg_rating'],2),
-            'num_ratings': rating_stats['num_ratings']
+                'avg_rating': round(rating_stats['avg_rating'], 2),
+                'num_ratings': rating_stats['num_ratings']
             }
 
     def _get_d(self):


Test patch: