Repository: readthedocs/readthedocs.org
Issue: MultipleObjectsReturned in core.views.serve.serve.docs

We get this exception while trying to serve some docs:

```
MultipleObjectsReturned: get() returned more than one Version -- it returned 2!
  File "django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "readthedocs/core/views/serve.py", line 96, in inner_view
    return view_func(request, project=project, *args, **kwargs)
  File "readthedocs/core/views/serve.py", line 74, in inner_view
    return view_func(request, subproject=subproject, *args, **kwargs)
  File "readthedocs/core/views/serve.py", line 156, in serve_docs
    version = project.versions.public(request.user).get(slug=version_slug)
  File "django/db/models/query.py", line 391, in get
    (self.model._meta.object_name, num)
```

The view code looks legit at a first look but there's a huge side effect of the user filtering in the public() method. It does not filter the projects by user but it adds to the queryset all the other user projects which is not what we need here:

```
# project of the version we are looking for
>>> versione.project
<Project: Documentazione API anagrafe del Comune di Tre Civette sul Comò>
# this queryset is fine
>>> versione.project.versions.all()
[<Version: Version master of Documentazione API anagrafe del Comune di Tre Civette sul Comò (861)>, <Version: Version bozza of Documentazione API anagrafe del Comune di Tre Civette sul Comò (860)>]
# but here we have other projects' version too
>>> versione.project.versions.public(user)
[<Version: Version stabile of design-docs (859)>, <Version: Version master of Documentazione API anagrafe del Comune di Tre Civette sul Comò (861)>, <Version: Version bozza of design-docs (852)>, <Version: Version bozza of Documentazione API anagrafe del Comune di Tre Civette sul Comò (860)>]

```

Changed files:
readthedocs/core/views/serve.py, readthedocs/api/v2/views/footer_views.py, readthedocs/rtd_tests/tests/test_doc_serving.py

diff --git a/readthedocs/api/v2/views/footer_views.py b/readthedocs/api/v2/views/footer_views.py
index 09709ac1b..04d4fe821 100644
--- a/readthedocs/api/v2/views/footer_views.py
+++ b/readthedocs/api/v2/views/footer_views.py
@@ -25,12 +25,15 @@ def get_version_compare_data(project, base_version=None):
     :param base_version: We assert whether or not the base_version is also the
                          highest version in the resulting "is_highest" value.
     """
-    versions_qs = project.versions.public().filter(active=True)
+    versions_qs = Version.objects.public(project=project)
 
     # Take preferences over tags only if the project has at least one tag
     if versions_qs.filter(type=TAG).exists():
         versions_qs = versions_qs.filter(type=TAG)
 
+    # Optimization
+    versions_qs = versions_qs.select_related('project')
+
     highest_version_obj, highest_version_comparable = highest_version(
         versions_qs,
     )
diff --git a/readthedocs/core/views/serve.py b/readthedocs/core/views/serve.py
index 390063f25..cd5ab8fdb 100644
--- a/readthedocs/core/views/serve.py
+++ b/readthedocs/core/views/serve.py
@@ -202,7 +202,11 @@ def serve_docs(
     if not version_slug:
         version_slug = project.get_default_version()
     try:
-        version = project.versions.public(request.user).get(slug=version_slug)
+        version = (
+            Version.objects
+            .public(user=request.user, project=project)
+            .get(slug=version_slug)
+        )
     except Version.DoesNotExist:
         # Properly raise a 404 if the version doesn't exist (or is inactive) and
         # a 401 if it does
@@ -409,8 +413,10 @@ def sitemap_xml(request, project):
 
         if project.translations.exists():
             for translation in project.translations.all():
-                translation_versions = translation.versions.public(
-                    ).values_list('slug', flat=True)
+                translation_versions = (
+                    Version.objects.public(project=translation)
+                    .values_list('slug', flat=True)
+                )
                 if version.slug in translation_versions:
                     href = project.get_docs_url(
                         version_slug=version.slug,
diff --git a/readthedocs/rtd_tests/tests/test_doc_serving.py b/readthedocs/rtd_tests/tests/test_doc_serving.py
index 134b19123..4f6cb9399 100644
--- a/readthedocs/rtd_tests/tests/test_doc_serving.py
+++ b/readthedocs/rtd_tests/tests/test_doc_serving.py
@@ -1,16 +1,16 @@
-
 import os
 
 import django_dynamic_fixture as fixture
 import mock
 from django.conf import settings
 from django.contrib.auth.models import User
-from django.http import Http404
+from django.http import Http404, HttpResponse
 from django.test import RequestFactory, TestCase
 from django.test.utils import override_settings
 from django.urls import reverse
 from mock import mock_open, patch
 
+from readthedocs.builds.constants import LATEST
 from readthedocs.builds.models import Version
 from readthedocs.core.middleware import SubdomainMiddleware
 from readthedocs.core.views import server_error_404_subdomain
@@ -293,3 +293,39 @@ class TestPublicDocs(BaseDocServing):
         # hreflang should use hyphen instead of underscore
         # in language and country value. (zh_CN should be zh-CN)
         self.assertContains(response, 'zh-CN')
+
+    @override_settings(
+        PYTHON_MEDIA=True,
+        USE_SUBDOMAIN=False,
+    )
+    @patch(
+        'readthedocs.core.views.serve._serve_symlink_docs',
+        new=mock.MagicMock(return_value=HttpResponse(content_type='text/html')),
+    )
+    def test_user_with_multiple_projects_serve_from_same_domain(self):
+        project = fixture.get(
+            Project,
+            main_language_project=None,
+            users=[self.eric],
+        )
+        other_project = fixture.get(
+            Project,
+            main_language_project=None,
+            users=[self.eric],
+        )
+
+        # Trigger the save method of the versions
+        project.versions.get(slug=LATEST).save()
+        other_project.versions.get(slug=LATEST).save()
+
+        # Two projects of one owner with versions with the same slug
+        self.assertIn(self.eric, project.users.all())
+        self.assertIn(self.eric, other_project.users.all())
+        self.assertTrue(project.versions.filter(slug=LATEST).exists())
+        self.assertTrue(other_project.versions.filter(slug=LATEST).exists())
+
+        self.client.force_login(self.eric)
+        request = self.client.get(
+            f'/docs/{project.slug}/en/latest/'
+        )
+        self.assertEqual(request.status_code, 200)