Repository: WordPress/openverse-api

Issue:
Related media view returns 500
## Description
<!-- Concisely describe the bug. Compare your experience with what you expected to happen. -->
<!-- For example: "I clicked the 'submit' button and instead of seeing a thank you message, I saw a blank page." -->
It would appear that the related media page on `main` is returning 500s, here's the exception I get when visiting a `related_url` on `main`:

```python
web_1               | [2022-07-20 17:45:23,475 - django.request - 241][ERROR] [e95d22f7b9f1444e90f44b09f435c2be] Internal Server Error: /v1/images/c608767c-a696-4d07-9bc1-daf174ca724d/related/
web_1               | Traceback (most recent call last):
web_1               |   File "/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
web_1               |     response = get_response(request)
web_1               |   File "/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
web_1               |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
web_1               |   File "/venv/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
web_1               |     return view_func(*args, **kwargs)
web_1               |   File "/venv/lib/python3.10/site-packages/rest_framework/viewsets.py", line 125, in view
web_1               |     return self.dispatch(request, *args, **kwargs)
web_1               |   File "/venv/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch
web_1               |     response = self.handle_exception(exc)
web_1               |   File "/venv/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception
web_1               |     self.raise_uncaught_exception(exc)
web_1               |   File "/venv/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
web_1               |     raise exc
web_1               |   File "/venv/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
web_1               |     response = handler(request, *args, **kwargs)
web_1               |   File "/venv/lib/python3.10/site-packages/django/utils/decorators.py", line 46, in _wrapper
web_1               |     return bound_method(*args, **kwargs)
web_1               |   File "/api/catalog/api/views/media_views.py", line 104, in related
web_1               |     results, num_results = related_media(
web_1               |   File "/api/catalog/api/controllers/elasticsearch/related.py", line 57, in related_media
web_1               |     f"results={json.dumps(results)}"
web_1               |   File "/usr/local/lib/python3.10/json/__init__.py", line 231, in dumps
web_1               |     return _default_encoder.encode(obj)
web_1               |   File "/usr/local/lib/python3.10/json/encoder.py", line 199, in encode
web_1               |     chunks = self.iterencode(o, _one_shot=True)
web_1               |   File "/usr/local/lib/python3.10/json/encoder.py", line 257, in iterencode
web_1               |     return _iterencode(o, 0)
web_1               |   File "/usr/local/lib/python3.10/json/encoder.py", line 179, in default
web_1               |     raise TypeError(f'Object of type {o.__class__.__name__} '
web_1               | TypeError: Object of type Hit is not JSON serializable
```

On first glance it looks like this might have been introduced in #790

## Reproduction
<!-- Provide detailed steps to reproduce the bug. -->
1. `just up`
2. Visit http://localhost:8000/v1/images/?q=car
3. Click on the `related_url` for a result
4. See error.

## Additional context
<!-- Add any other context about the problem here; or delete the section entirely. -->

## Resolution
<!-- Replace the [ ] with [x] to check the box. -->
- [ ] 🙋 I would be interested in resolving this bug.



Discussion:
Ah! I swore I had squashed all those.

Why do the integration tests not catch this, I wonder?

To fix this we need to call `as_dict` on the `Hit` object.

Resolving patch:
diff --git a/api/catalog/api/controllers/elasticsearch/related.py b/api/catalog/api/controllers/elasticsearch/related.py
--- a/api/catalog/api/controllers/elasticsearch/related.py
+++ b/api/catalog/api/controllers/elasticsearch/related.py
@@ -2,6 +2,7 @@
 import logging
 
 from elasticsearch_dsl import Search
+from elasticsearch_dsl.response import Hit
 
 from catalog.api.controllers.elasticsearch.utils import (
     exclude_filtered_providers,
@@ -51,9 +52,16 @@ def related_media(uuid, index, filter_dead):
 
     result_count, _ = get_result_and_page_count(response, results, page_size)
 
+    dumpable_results = (
+        results.to_dict()
+        if isinstance(results, Hit)
+        else [r.to_dict() for r in results]
+    )
+
     logger.debug(
         "finished post processing and returning related "
         f"result_count={result_count} "
-        f"results={json.dumps(results)}"
+        f"results={json.dumps(dumpable_results)}"
     )
+
     return results, result_count
diff --git a/api/catalog/api/controllers/elasticsearch/search.py b/api/catalog/api/controllers/elasticsearch/search.py
--- a/api/catalog/api/controllers/elasticsearch/search.py
+++ b/api/catalog/api/controllers/elasticsearch/search.py
@@ -248,7 +248,7 @@ def perform_search(
     dumpable_results = (
         results.to_dict()
         if isinstance(results, Hit)
-        else list(map(lambda r: r.to_dict(), results))
+        else [r.to_dict() for r in results]
     )
 
     logger.debug(


Test patch:
diff --git a/api/test/audio_integration_test.py b/api/test/audio_integration_test.py
--- a/api/test/audio_integration_test.py
+++ b/api/test/audio_integration_test.py
@@ -8,6 +8,7 @@
 from test.media_integration import (
     detail,
     license_filter_case_insensitivity,
+    related,
     report,
     search,
     search_all_excluded,
@@ -122,3 +123,7 @@ def test_audio_uuid_validation():
     uuid_validation("audio", "123456789123456789123456789123456789")
     uuid_validation("audio", "12345678-1234-5678-1234-1234567891234")
     uuid_validation("audio", "abcd")
+
+
+def test_image_related(audio_fixture):
+    related(audio_fixture)
diff --git a/api/test/image_integration_test.py b/api/test/image_integration_test.py
--- a/api/test/image_integration_test.py
+++ b/api/test/image_integration_test.py
@@ -9,6 +9,7 @@
 from test.media_integration import (
     detail,
     license_filter_case_insensitivity,
+    related,
     report,
     search,
     search_all_excluded,
@@ -139,3 +140,7 @@ def test_image_uuid_validation():
     uuid_validation("images", "123456789123456789123456789123456789")
     uuid_validation("images", "12345678-1234-5678-1234-1234567891234")
     uuid_validation("images", "abcd")
+
+
+def test_image_related(image_fixture):
+    related(image_fixture)
diff --git a/api/test/media_integration.py b/api/test/media_integration.py
--- a/api/test/media_integration.py
+++ b/api/test/media_integration.py
@@ -169,3 +169,9 @@ def license_filter_case_insensitivity(media_type):
 def uuid_validation(media_type, identifier):
     response = requests.get(f"{API_URL}/v1/{media_type}/{identifier}", verify=False)
     assert response.status_code == 404
+
+
+def related(fixture):
+    related_url = fixture["results"][0]["related_url"]
+    response = requests.get(related_url)
+    assert response.status_code == 200