Repository: digitalfabrik/integreat-cms

Issue:
Feedback endpoint not working if multiple translations exist for same slug
### Describe the Bug
<!-- A clear and concise description of what the bug is. -->
When a translation has multiple versions with the same slug, the feedback endpoint throws an error:

### Steps to Reproduce

1. Create a second translation for `willkommen` page with same slug
2. execute `curl -X "POST" "http://localhost:8000/api/augsburg/de/feedback/page" -d 'slug=willkommen&rating=up'`

### Expected Behavior
<!-- A clear and concise description of what you expected to happen. -->
`{"success": "Feedback successfully submitted"}`

### Actual Behavior
<!-- A clear and concise description of what actually happened. -->
Internal server error

### Additional Information
<!-- Add any other context (e.g. logs, screenshots, etc.) about the problem here. -->

<details>
  <summary>Traceback</summary>
    
  ```
Mar 06 20:43:18 ERROR django.request - Internal Server Error: /api/augsburg/de/feedback/page
Traceback (most recent call last):
  File "/home/timo/job/integreat/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/timo/job/integreat/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/timo/job/integreat/integreat-cms/.venv/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/timo/job/integreat/integreat-cms/integreat_cms/api/decorators.py", line 87, in handle_feedback
    return func(data, region, language, comment, rating_normalized, is_technical)
  File "/home/timo/job/integreat/integreat-cms/integreat_cms/api/decorators.py", line 124, in wrap
    return function(request, *args, **kwargs)
  File "/home/timo/job/integreat/integreat-cms/integreat_cms/api/v3/feedback/page_feedback.py", line 38, in page_feedback
    return page_feedback_internal(data, region, language, comment, rating, is_technical)
  File "/home/timo/job/integreat/integreat-cms/integreat_cms/api/v3/feedback/page_feedback.py", line 67, in page_feedback_internal
    page_translation = get_object_or_404(
  File "/home/timo/job/integreat/integreat-cms/.venv/lib/python3.9/site-packages/django/shortcuts.py", line 76, in get_object_or_404
    return queryset.get(*args, **kwargs)
  File "/home/timo/job/integreat/integreat-cms/.venv/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
    raise self.model.MultipleObjectsReturned(
integreat_cms.cms.models.pages.page_translation.PageTranslation.MultipleObjectsReturned: get() returned more than one PageTranslation -- it returned 2!
  ```
  
</details>


Discussion:

Resolving patch:
diff --git a/integreat_cms/api/v3/feedback/event_feedback.py b/integreat_cms/api/v3/feedback/event_feedback.py
--- a/integreat_cms/api/v3/feedback/event_feedback.py
+++ b/integreat_cms/api/v3/feedback/event_feedback.py
@@ -1,9 +1,12 @@
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404
+import logging
 
-from ....cms.models import EventFeedback, EventTranslation
+from django.http import JsonResponse, Http404
+
+from ....cms.models import EventFeedback
 from ...decorators import json_response, feedback_handler
 
+logger = logging.getLogger(__name__)
+
 
 @feedback_handler
 @json_response
@@ -59,13 +62,29 @@ def event_feedback_internal(data, region, language, comment, rating, is_technica
     :param is_technical: is feedback on content or on tech
     :type is_technical: bool
 
+    :raises ~django.http.Http404: HTTP status 404 if no event with the given slug exists.
+
     :return: JSON object according to APIv3 single page feedback endpoint definition
     :rtype: ~django.http.JsonResponse
     """
-
-    event_translation = get_object_or_404(
-        EventTranslation, event__region=region, language=language, slug=data.get("slug")
-    )
+    event_translation_slug = data.get("slug")
+
+    events = region.events.filter(
+        translations__slug=data.get("slug"),
+        translations__language=language,
+    ).distinct()
+
+    if len(events) > 1:
+        logger.error(
+            "Event translation slug %r is not unique per region and language, found multiple: %r",
+            event_translation_slug,
+            events,
+        )
+        return JsonResponse({"error": "Internal Server Error"}, status=500)
+    if len(events) == 0:
+        raise Http404("No matching event found for slug.")
+    event = events[0]
+    event_translation = event.get_translation(language.slug)
 
     EventFeedback.objects.create(
         event_translation=event_translation,
diff --git a/integreat_cms/api/v3/feedback/page_feedback.py b/integreat_cms/api/v3/feedback/page_feedback.py
--- a/integreat_cms/api/v3/feedback/page_feedback.py
+++ b/integreat_cms/api/v3/feedback/page_feedback.py
@@ -1,12 +1,15 @@
 """
 APIv3 endpoint for feedback bout single pages
 """
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404
+import logging
 
-from ....cms.models import PageFeedback, PageTranslation
+from django.http import JsonResponse, Http404
+
+from ....cms.models import PageFeedback
 from ...decorators import json_response, feedback_handler
 
+logger = logging.getLogger(__name__)
+
 
 @feedback_handler
 @json_response
@@ -60,13 +63,29 @@ def page_feedback_internal(data, region, language, comment, rating, is_technical
     :param is_technical: is feedback on content or on tech
     :type is_technical: bool
 
+    :raises ~django.http.Http404: HTTP status 404 if no page with the given slug exists.
+
     :return: JSON object according to APIv3 single page feedback endpoint definition
     :rtype: ~django.http.JsonResponse
     """
-
-    page_translation = get_object_or_404(
-        PageTranslation, page__region=region, language=language, slug=data.get("slug")
-    )
+    page_translation_slug = data.get("slug")
+
+    pages = region.pages.filter(
+        translations__slug=data.get("slug"),
+        translations__language=language,
+    ).distinct()
+
+    if len(pages) > 1:
+        logger.error(
+            "Page translation slug %r is not unique per region and language, found multiple: %r",
+            page_translation_slug,
+            pages,
+        )
+        return JsonResponse({"error": "Internal Server Error"}, status=500)
+    if len(pages) == 0:
+        raise Http404("No matching page found for slug.")
+    page = pages[0]
+    page_translation = page.get_translation(language.slug)
 
     PageFeedback.objects.create(
         page_translation=page_translation,
diff --git a/integreat_cms/api/v3/feedback/poi_feedback.py b/integreat_cms/api/v3/feedback/poi_feedback.py
--- a/integreat_cms/api/v3/feedback/poi_feedback.py
+++ b/integreat_cms/api/v3/feedback/poi_feedback.py
@@ -1,9 +1,12 @@
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404
+import logging
 
-from ....cms.models import POIFeedback, POITranslation
+from django.http import JsonResponse, Http404
+
+from ....cms.models import POIFeedback
 from ...decorators import json_response, feedback_handler
 
+logger = logging.getLogger(__name__)
+
 
 @feedback_handler
 @json_response
@@ -29,12 +32,29 @@ def poi_feedback(data, region, language, comment, rating, is_technical):
     :param is_technical: is feedback on content or on tech
     :type is_technical: bool
 
+    :raises ~django.http.Http404: HTTP status 404 if no POI with the given slug exists.
+
     :return: decorated function that saves feedback in database
     :rtype: ~collections.abc.Callable
     """
-    poi_translation = get_object_or_404(
-        POITranslation, poi__region=region, language=language, slug=data.get("slug")
-    )
+    poi_translation_slug = data.get("slug")
+
+    pois = region.pois.filter(
+        translations__slug=data.get("slug"),
+        translations__language=language,
+    ).distinct()
+
+    if len(pois) > 1:
+        logger.error(
+            "POI translation slug %r is not unique per region and language, found multiple: %r",
+            poi_translation_slug,
+            pois,
+        )
+        return JsonResponse({"error": "Internal Server Error"}, status=500)
+    if len(pois) == 0:
+        raise Http404("No matching location found for slug.")
+    poi = pois[0]
+    poi_translation = poi.get_translation(language.slug)
 
     POIFeedback.objects.create(
         poi_translation=poi_translation,


Test patch:
diff --git a/integreat_cms/cms/fixtures/test_data.json b/integreat_cms/cms/fixtures/test_data.json
--- a/integreat_cms/cms/fixtures/test_data.json
+++ b/integreat_cms/cms/fixtures/test_data.json
@@ -460,11 +460,28 @@
       "title": "Willkommen",
       "slug": "willkommen",
       "status": "PUBLIC",
-      "content": "<p>Willkommen in Augsburg</p>",
+      "content": "<p>Erste Version</p>",
       "language": 1,
       "currently_in_translation": false,
       "version": 1,
       "minor_edit": false,
+      "last_updated": "2019-08-11T07:57:42.456Z",
+      "creator": 1,
+      "page": 1
+    }
+  },
+  {
+    "model": "cms.pagetranslation",
+    "pk": 75,
+    "fields": {
+      "title": "Willkommen",
+      "slug": "willkommen",
+      "status": "PUBLIC",
+      "content": "<p>Willkommen in Augsburg</p>",
+      "language": 1,
+      "currently_in_translation": false,
+      "version": 2,
+      "minor_edit": false,
       "last_updated": "2019-08-12T07:57:42.456Z",
       "creator": 1,
       "page": 1
@@ -2306,18 +2323,35 @@
       "slug": "test-veranstaltung",
       "status": "PUBLIC",
       "title": "Test-Veranstaltung",
-      "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
+      "content": "<p>Erste Version</p>",
       "language": 1,
       "currently_in_translation": false,
       "version": 1,
       "minor_edit": false,
       "creator": 1,
-      "last_updated": "2020-01-22T12:46:33.967Z"
+      "last_updated": "2020-01-21T12:46:33.967Z"
     }
   },
   {
     "model": "cms.eventtranslation",
     "pk": 2,
+    "fields": {
+      "event": 1,
+      "slug": "test-veranstaltung",
+      "status": "PUBLIC",
+      "title": "Test-Veranstaltung",
+      "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
+      "language": 1,
+      "currently_in_translation": false,
+      "version": 2,
+      "minor_edit": false,
+      "creator": 1,
+      "last_updated": "2020-01-22T12:46:33.967Z"
+    }
+  },
+  {
+    "model": "cms.eventtranslation",
+    "pk": 3,
     "fields": {
       "event": 1,
       "slug": "test-event",
@@ -2355,17 +2389,34 @@
       "poi": 4,
       "status": "PUBLIC",
       "short_description": "Kurzbeschreibung des Test-Ortes",
-      "content": "<p>Beschreibung des Test-Ortes</p>",
+      "content": "<p>Erste Version</p>",
       "language": 1,
       "version": 1,
       "minor_edit": false,
-      "last_updated": "2020-01-22T12:52:13.616Z",
+      "last_updated": "2020-01-21T12:52:13.616Z",
       "creator": 1
     }
   },
   {
     "model": "cms.poitranslation",
     "pk": 2,
+    "fields": {
+      "title": "Test-Ort",
+      "slug": "test-ort",
+      "poi": 4,
+      "status": "PUBLIC",
+      "short_description": "Kurzbeschreibung des Test-Ortes",
+      "content": "<p>Beschreibung des Test-Ortes</p>",
+      "language": 1,
+      "version": 2,
+      "minor_edit": false,
+      "last_updated": "2020-01-22T12:52:13.616Z",
+      "creator": 1
+    }
+  },
+  {
+    "model": "cms.poitranslation",
+    "pk": 3,
     "fields": {
       "title": "Test location",
       "slug": "test-location",
diff --git a/tests/api/expected-outputs/api_augsburg_de_children_depth_2.json b/tests/api/expected-outputs/api_augsburg_de_children_depth_2.json
--- a/tests/api/expected-outputs/api_augsburg_de_children_depth_2.json
+++ b/tests/api/expected-outputs/api_augsburg_de_children_depth_2.json
@@ -1,6 +1,6 @@
 [
     {
-        "id": 1,
+        "id": 75,
         "url": "http://localhost:8000/augsburg/de/willkommen/",
         "path": "/augsburg/de/willkommen/",
         "title": "Willkommen",
diff --git a/tests/api/expected-outputs/augsburg_de_children.json b/tests/api/expected-outputs/augsburg_de_children.json
--- a/tests/api/expected-outputs/augsburg_de_children.json
+++ b/tests/api/expected-outputs/augsburg_de_children.json
@@ -1,6 +1,6 @@
 [
     {
-        "id": 1,
+        "id": 75,
         "url": "http://localhost:8000/augsburg/de/willkommen/",
         "path": "/augsburg/de/willkommen/",
         "title": "Willkommen",
diff --git a/tests/api/expected-outputs/augsburg_de_events.json b/tests/api/expected-outputs/augsburg_de_events.json
--- a/tests/api/expected-outputs/augsburg_de_events.json
+++ b/tests/api/expected-outputs/augsburg_de_events.json
@@ -1,6 +1,6 @@
 [
     {
-        "id": 1,
+        "id": 2,
         "url": "http://localhost:8000/augsburg/de/events/test-veranstaltung/",
         "path": "/augsburg/de/events/test-veranstaltung/",
         "title": "Test-Veranstaltung",
@@ -9,7 +9,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
@@ -49,7 +49,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
@@ -89,7 +89,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
@@ -129,7 +129,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
@@ -169,7 +169,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
@@ -209,7 +209,7 @@
         "content": "<p>Dies ist die Beschreibung einer Test-Veranstaltung.</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/events/test-event/",
                 "path": "/augsburg/en/events/test-event/"
             }
diff --git a/tests/api/expected-outputs/augsburg_de_locations.json b/tests/api/expected-outputs/augsburg_de_locations.json
--- a/tests/api/expected-outputs/augsburg_de_locations.json
+++ b/tests/api/expected-outputs/augsburg_de_locations.json
@@ -1,6 +1,6 @@
 [
     {
-        "id": 1,
+        "id": 2,
         "url": "http://localhost:8000/augsburg/de/locations/test-ort/",
         "path": "/augsburg/de/locations/test-ort/",
         "title": "Test-Ort",
@@ -9,7 +9,7 @@
         "content": "<p>Beschreibung des Test-Ortes</p>",
         "available_languages": {
             "en": {
-                "id": 2,
+                "id": 3,
                 "url": "http://localhost:8000/augsburg/en/locations/test-location/",
                 "path": "/augsburg/en/locations/test-location/"
             }
diff --git a/tests/api/expected-outputs/augsburg_de_page_1.json b/tests/api/expected-outputs/augsburg_de_page_1.json
--- a/tests/api/expected-outputs/augsburg_de_page_1.json
+++ b/tests/api/expected-outputs/augsburg_de_page_1.json
@@ -1,5 +1,5 @@
 {
-    "id": 1,
+    "id": 75,
     "url": "http://localhost:8000/augsburg/de/willkommen/",
     "path": "/augsburg/de/willkommen/",
     "title": "Willkommen",
diff --git a/tests/api/expected-outputs/augsburg_de_pages.json b/tests/api/expected-outputs/augsburg_de_pages.json
--- a/tests/api/expected-outputs/augsburg_de_pages.json
+++ b/tests/api/expected-outputs/augsburg_de_pages.json
@@ -1,6 +1,6 @@
 [
     {
-        "id": 1,
+        "id": 75,
         "url": "http://localhost:8000/augsburg/de/willkommen/",
         "path": "/augsburg/de/willkommen/",
         "title": "Willkommen",