Repository: readthedocs/readthedocs.org

Issue:
List only built versions on the footer
When a version is `built=False` our code links to the version detail page

https://github.com/readthedocs/readthedocs.org/blob/89a58a07716ccf5793e0a241be174d52d1f03c76/readthedocs/builds/models.py#L296-L303

This method gets called from `sort_version_aware`, here we are passing all active versions (including not built).

https://github.com/readthedocs/readthedocs.org/blob/89a58a07716ccf5793e0a241be174d52d1f03c76/readthedocs/projects/models.py#L953-L986

This is a problem in .com, where we don't have access to that `URLCONF`, so reverse doesn't work.

I'm thinking two options:

1. Only list built versions, this is modify this method and add a filter (this method is only used in the footer).

https://github.com/readthedocs/readthedocs.org/blob/89a58a07716ccf5793e0a241be174d52d1f03c76/readthedocs/projects/models.py#L953

2. Only change this in .com, adding a new kwarg to .public (`only_builts=False`)

I'm more inclined to 1, so we affect .org and .com,  giving a link to the dashboard to a user that doesn't have permissions doesn't look correct.


Discussion:
Just want to note a use case of this.

Listing "`active` but not `built` versions" in the footer is _kind of a hidden feature_. Why? It allows people to list all the supported versions in the footer and create a `Redirect` pointing to a different hosting platform for old versions for example.

So, 1.x and 2.x are hosted at Read the Docs, but 0.8.x and 0.9.x are hosted in a different platform. You `active=True` the fourth of them and create two Redirects for 0.8.x and 0.9.x.

I've used this "hidden feature" for a user already.
@humitos but when the version is `built=False` it links to the dashboard, so the redirect doesn't work there.
Yea, I think linking to the dashboard doesn't make sense in the context of the footer. I'm happy to update the link to simply go to the docs page, and have it 404 if it isn't built. That enables the redirect logic that @humitos was talking about, and linking the dashboard is not useful for users basically ever when they're in the docs page. 

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
@@ -44,7 +44,9 @@ def get_version_compare_data(project, base_version=None):
         'is_highest': True,
     }
     if highest_version_obj:
-        ret_val['url'] = highest_version_obj.get_absolute_url()
+        # Never link to the dashboard,
+        # users reading the docs may don't have access to the dashboard.
+        ret_val['url'] = highest_version_obj.get_absolute_url(link_to_dashboard=False)
         ret_val['slug'] = highest_version_obj.slug
     if base_version and base_version.slug != LATEST:
         try:
diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py
--- a/readthedocs/builds/models.py
+++ b/readthedocs/builds/models.py
@@ -77,7 +77,6 @@
 from readthedocs.projects.models import APIProject, Project
 from readthedocs.projects.version_handling import determine_stable_version
 
-
 log = logging.getLogger(__name__)
 
 
@@ -289,7 +288,13 @@ def commit_name(self):
         )
         return self.identifier
 
-    def get_absolute_url(self):
+    def get_absolute_url(self, link_to_dashboard=True):
+        """
+        Get absolute url to the docs of the version.
+
+        :param link_to_dashboard: If `False` we never try to link to the dashboard,
+                                  we link to the docs even if they result in a 404.
+        """
         # Hack external versions for now.
         # TODO: We can integrate them into the resolver
         # but this is much simpler to handle since we only link them a couple places for now
@@ -299,7 +304,7 @@ def get_absolute_url(self):
                 f'{self.project.slug}/{self.slug}/index.html'
             return url
 
-        if not self.built and not self.uploaded:
+        if not self.built and not self.uploaded and link_to_dashboard:
             return reverse(
                 'project_version_detail',
                 kwargs={


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_footer.py b/readthedocs/rtd_tests/tests/test_footer.py
--- a/readthedocs/rtd_tests/tests/test_footer.py
+++ b/readthedocs/rtd_tests/tests/test_footer.py
@@ -1,6 +1,7 @@
 import mock
 from django.contrib.sessions.backends.base import SessionBase
 from django.test import TestCase
+from django.test.utils import override_settings
 from rest_framework.test import APIRequestFactory, APITestCase
 
 from readthedocs.api.v2.views.footer_views import (
@@ -114,6 +115,10 @@ def test_not_show_edit_on_github(self):
         self.assertNotIn('Edit', response.data['html'])
 
 
+@override_settings(
+    USE_SUBDOMAIN=True,
+    PUBLIC_DOMAIN='readthedocs.io',
+)
 class TestVersionCompareFooter(TestCase):
     fixtures = ['test_data']
 
@@ -124,7 +129,7 @@ def test_highest_version_from_stable(self):
         base_version = self.pip.get_stable_version()
         valid_data = {
             'project': 'Version 0.8.1 of Pip (19)',
-            'url': '/dashboard/pip/version/0.8.1/',
+            'url': 'http://pip.readthedocs.io/en/0.8.1/',
             'slug': '0.8.1',
             'version': '0.8.1',
             'is_highest': True,
@@ -136,7 +141,7 @@ def test_highest_version_from_lower(self):
         base_version = self.pip.versions.get(slug='0.8')
         valid_data = {
             'project': 'Version 0.8.1 of Pip (19)',
-            'url': '/dashboard/pip/version/0.8.1/',
+            'url': 'http://pip.readthedocs.io/en/0.8.1/',
             'slug': '0.8.1',
             'version': '0.8.1',
             'is_highest': False,
@@ -149,7 +154,7 @@ def test_highest_version_from_latest(self):
         base_version = self.pip.versions.get(slug=LATEST)
         valid_data = {
             'project': 'Version 0.8.1 of Pip (19)',
-            'url': '/dashboard/pip/version/0.8.1/',
+            'url': 'http://pip.readthedocs.io/en/0.8.1/',
             'slug': '0.8.1',
             'version': '0.8.1',
             'is_highest': True,
@@ -177,7 +182,7 @@ def test_highest_version_over_branches(self):
         base_version = self.pip.versions.get(slug='0.8.1')
         valid_data = {
             'project': 'Version 1.0.0 of Pip ({})'.format(version.pk),
-            'url': '/dashboard/pip/version/1.0.0/',
+            'url': 'http://pip.readthedocs.io/en/1.0.0/',
             'slug': '1.0.0',
             'version': '1.0.0',
             'is_highest': False,
@@ -191,7 +196,7 @@ def test_highest_version_without_tags(self):
         base_version = self.pip.versions.get(slug='0.8.1')
         valid_data = {
             'project': 'Version 0.8.1 of Pip (19)',
-            'url': '/dashboard/pip/version/0.8.1/',
+            'url': 'http://pip.readthedocs.io/en/0.8.1/',
             'slug': '0.8.1',
             'version': '0.8.1',
             'is_highest': True,
@@ -202,7 +207,7 @@ def test_highest_version_without_tags(self):
         base_version = self.pip.versions.get(slug='0.8')
         valid_data = {
             'project': 'Version 0.8.1 of Pip (19)',
-            'url': '/dashboard/pip/version/0.8.1/',
+            'url': 'http://pip.readthedocs.io/en/0.8.1/',
             'slug': '0.8.1',
             'version': '0.8.1',
             'is_highest': False,
@@ -219,7 +224,7 @@ def test_highest_version_without_tags(self):
         )
         valid_data = {
             'project': 'Version 2.0.0 of Pip ({})'.format(version.pk),
-            'url': '/dashboard/pip/version/2.0.0/',
+            'url': 'http://pip.readthedocs.io/en/2.0.0/',
             'slug': '2.0.0',
             'version': '2.0.0',
             'is_highest': False,
@@ -235,11 +240,12 @@ class TestFooterPerformance(APITestCase):
 
     # The expected number of queries for generating the footer
     # This shouldn't increase unless we modify the footer API
-    EXPECTED_QUERIES = 9
+    EXPECTED_QUERIES = 13
 
     def setUp(self):
         self.pip = Project.objects.get(slug='pip')
         self.pip.versions.create_latest()
+        self.pip.versions.update(built=True)
 
     def render(self):
         request = self.factory.get(self.url)