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)>]

```
core: fix MultipleObjectsReturned in 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.

Instead simplify the code to:
- return 404 if the requested version does not exist
- return 401 if the version is private and the user is not admin
- serve the file if the version is private and the user is a project admin

Fix #4350


Discussion:
Wondering if not passing the user to public is the correct fix.
I have encountered this sometimes locally, but I wasn't able to replicate, and I think this has never happened in production.
Also, this had already been reported before https://github.com/rtfd/readthedocs.org/issues/2613
Thanks for the heads up. I'll try to cook a test and a PR then.
Just a short question, why is this issue closed? To me it appears that the fix has not been applied to readthedocs but to the fork only.
@p3trus thanks for checking, it has been closed automatically by github :)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Any plans to merge this bugfix in the near future?
@p3trus this is the bug not the PR
I know but the docsitalia team already fixed it.
@p3trus you probably missed I'm the one who is trying to get this merged :)
@xrmx I didn't mean to sound nagging. Just wanted to know the timeframe if there is any.
@p3trus asking for ETA is nagging :) If you look at the PR you'll see clearly stated that it's not merged because coverage tests fail. They fail because code does not do what the tests expect and I suspect even before the proposed changes.
Ping
Thanks @xrmx. I'm speaking for myself here, but I'm not sure there's a need to ping for cases like these. The maintainers know the PR is here, and they'll get to it when they can. 
Sorry, I don't want to nag but the time i can spend on RTD is limited and will end next month so I'd like to push the more fixes i can upstream.
@xrmx I really appreciate that. And I also very sorry, the current core team is in a very large migration, so probably this (and others) PR will not merge very soon :(
# [Codecov](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356?src=pr&el=h1) Report
> Merging [#4356](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356?src=pr&el=desc) into [master](https://codecov.io/gh/rtfd/readthedocs.org/commit/a992ad1a2695d6d6f2396f67af2163abac2a22d0?src=pr&el=desc) will **decrease** coverage by `2.31%`.
> The diff coverage is `50%`.

```diff
@@            Coverage Diff            @@
##           master   #4356      +/-   ##
=========================================
- Coverage   78.72%   76.4%   -2.32%     
=========================================
  Files         164     158       -6     
  Lines       10107    9991     -116     
  Branches     1282    1263      -19     
=========================================
- Hits         7957    7634     -323     
- Misses       1820    2017     +197     
- Partials      330     340      +10
```

| [Impacted Files](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [readthedocs/core/views/serve.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvY29yZS92aWV3cy9zZXJ2ZS5weQ==) | `86.4% <50%> (-2.26%)` | :arrow_down: |
| [readthedocs/search/utils.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3Mvc2VhcmNoL3V0aWxzLnB5) | `0% <0%> (-53.66%)` | :arrow_down: |
| [readthedocs/core/models.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvY29yZS9tb2RlbHMucHk=) | `74.19% <0%> (-20.26%)` | :arrow_down: |
| [readthedocs/search/parse\_json.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3Mvc2VhcmNoL3BhcnNlX2pzb24ucHk=) | `57.14% <0%> (-11.52%)` | :arrow_down: |
| [readthedocs/vcs\_support/backends/svn.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvdmNzX3N1cHBvcnQvYmFja2VuZHMvc3ZuLnB5) | `27.69% <0%> (-9.81%)` | :arrow_down: |
| [readthedocs/restapi/urls.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvcmVzdGFwaS91cmxzLnB5) | `80% <0%> (-9.66%)` | :arrow_down: |
| [readthedocs/core/utils/tasks/permission\_checks.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvY29yZS91dGlscy90YXNrcy9wZXJtaXNzaW9uX2NoZWNrcy5weQ==) | `28.57% <0%> (-8.93%)` | :arrow_down: |
| [readthedocs/restapi/utils.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvcmVzdGFwaS91dGlscy5weQ==) | `91.3% <0%> (-8.7%)` | :arrow_down: |
| [readthedocs/builds/models.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvYnVpbGRzL21vZGVscy5weQ==) | `75.6% <0%> (-8.22%)` | :arrow_down: |
| [readthedocs/doc\_builder/config.py](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree#diff-cmVhZHRoZWRvY3MvZG9jX2J1aWxkZXIvY29uZmlnLnB5) | `91.89% <0%> (-8.11%)` | :arrow_down: |
| ... and [159 more](https://codecov.io/gh/rtfd/readthedocs.org/pull/4356/diff?src=pr&el=tree-more) | |

Rebased the PR on latest master
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Dear stale bot i plan to refresh this when i have time.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale bot please keep it open
I faced this problem again locally. The solution looks correct, but I still not sure why this is happening randomly, makes me think the problem is in some other place, maybe we are mutating something.
@stsewd yeah, still there's something fishy with tests
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This is green, missing review :)
Today I had this problem, digging a little more, the real issue comes from

https://github.com/rtfd/readthedocs.org/blob/45df7fd0da44be9eab3c0cb2888f6a9a15421fc5/readthedocs/builds/querysets.py#L22-L24

`get_objects_for_user` returns all versions of all its projects, but in the serve_docs function

https://github.com/rtfd/readthedocs.org/blob/45df7fd0da44be9eab3c0cb2888f6a9a15421fc5/readthedocs/core/views/serve.py#L205

we expect to only have versions of one project.

Resolving patch:
diff --git a/readthedocs/api/v2/views/footer_views.py b/readthedocs/api/v2/views/footer_views.py
--- 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
--- 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 changefreqs_generator():
 
         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,


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_doc_serving.py b/readthedocs/rtd_tests/tests/test_doc_serving.py
--- 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 @@ def test_sitemap_xml(self):
         # 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)