Skip to content

Commit fc8adb7

Browse files
authored
Merge pull request #6792 from readthedocs/implement-hidden-state-for-versions
Implement hidden state for versions
2 parents 456dd93 + 965dfc0 commit fc8adb7

24 files changed

+341
-21
lines changed
Loading

docs/api/v3.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ Version detail
394394
"ref": "19.0.2",
395395
"built": true,
396396
"active": true,
397+
"hidden": false,
397398
"type": "tag",
398399
"last_build": "{BUILD}",
399400
"downloads": {
@@ -460,7 +461,8 @@ Version update
460461
.. sourcecode:: json
461462

462463
{
463-
"active": true
464+
"active": true,
465+
"hidden": false
464466
}
465467

466468
:statuscode 204: Updated successfully

docs/guides/hiding-a-version.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Hide a Version and Keep its Docs Online
2+
=======================================
3+
4+
If you manage a project with a lot of versions,
5+
the version (flyout) menu of your docs can be easily overwhelmed and hard to navigate.
6+
7+
.. figure:: /_static/images/guides/flyout-overwhelmed.png
8+
:align: center
9+
10+
Overwhelmed flyout menu
11+
12+
You can deactivate the version to remove its docs,
13+
but removing its docs isn't always an option.
14+
To not list a version in the flyout menu while keeping its docs online, you can mark it as hidden.
15+
Go to the :guilabel:`Versions` tab of your project, click on :guilabel:`Edit` and mark the ``Hidden`` option.
16+
17+
Users that have a link to your old version will still be able to see your docs.
18+
And new users can see all your versions (including hidden versions) in the versions tab of your project at ``https://readthedocs.org/projects/<your-project>/versions/``
19+
20+
Check the docs about :ref:`versions' states <versions:States>` for more information.

docs/guides/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,16 @@ These guides will help you customize or tune aspects of Read the Docs.
3636
autobuild-docs-for-pull-requests
3737
build-notifications
3838
build-using-too-many-resources
39-
technical-docs-seo-guide
4039
canonical
4140
conda
4241
environment-variables
4342
feature-flags
4443
google-analytics
44+
hiding-a-version
4545
searching-with-readthedocs
4646
sitemaps
4747
specifying-dependencies
48+
technical-docs-seo-guide
4849
wipe-environment
4950

5051

docs/versions.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,53 @@ they will be redirected to the **Default version**.
5858
This defaults to **latest**,
5959
but could also point to your latest released version.
6060

61+
States
62+
------
63+
64+
States define the visibility of a version across the site.
65+
You can change the states of a version from the :guilabel:`Versions` tab of your project.
66+
67+
Active
68+
~~~~~~
69+
70+
- **Active**
71+
72+
- Docs for this version are visible
73+
- Builds can be triggered for this version
74+
75+
- **Inactive**
76+
77+
- Docs for this version aren't visible
78+
- Builds can't be triggered for this version
79+
80+
When you deactivate a version, its docs are removed.
81+
82+
Hidden
83+
~~~~~~
84+
85+
- **Not hidden and Active**
86+
87+
- This version is listed on the version (flyout) menu on the docs site
88+
- This version is shown in search results on the docs site
89+
90+
- **Hidden and Active**
91+
92+
- This version isn't listed on the version (flyout) menu on the docs site
93+
- This version isn't show in search results from another version on the docs site
94+
(like on search results from a superproject)
95+
96+
Hiding a version doesn't make it private,
97+
any user with a link to its docs would be able to see it.
98+
This is useful when:
99+
100+
- You no longer support a version, but you don't want to remove its docs.
101+
- You have a work in progress version and don't want to publish its docs just yet.
102+
103+
.. note::
104+
105+
Active versions that are hidden will be listed as ``Disallow: /path/to/version/``
106+
in the default `robots.txt file <https://www.robotstxt.org/>`__ created by Read the Docs.
107+
61108
Version warning
62109
---------------
63110

readthedocs/api/v2/views/footer_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def _get_active_versions_sorted(self):
127127
project = self._get_project()
128128
versions = project.ordered_active_versions(
129129
user=self.request.user,
130+
include_hidden=False,
130131
)
131132
return versions
132133

readthedocs/api/v3/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class Meta:
220220
'ref',
221221
'built',
222222
'active',
223+
'hidden',
223224
'type',
224225
'downloads',
225226
'urls',

readthedocs/api/v3/tests/responses/projects-detail.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"active_versions": [
33
{
44
"active": true,
5+
"hidden": false,
56
"built": true,
67
"downloads": {
78
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",

readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"triggered": true,
7373
"version": {
7474
"active": true,
75+
"hidden": false,
7576
"built": true,
7677
"downloads": {
7778
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",

readthedocs/api/v3/tests/responses/projects-versions-detail.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"active": true,
3+
"hidden": false,
34
"built": true,
45
"downloads": {
56
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",

readthedocs/builds/forms.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import re
44
import textwrap
55

6+
from crispy_forms.helper import FormHelper
7+
from crispy_forms.layout import HTML, Fieldset, Layout
68
from django import forms
9+
from django.template.loader import render_to_string
710
from django.utils.translation import ugettext_lazy as _
811

912
from readthedocs.builds.constants import (
@@ -22,7 +25,36 @@ class VersionForm(HideProtectedLevelMixin, forms.ModelForm):
2225

2326
class Meta:
2427
model = Version
25-
fields = ['active', 'privacy_level']
28+
states_fields = ['active', 'hidden']
29+
privacy_fields = ['privacy_level']
30+
fields = (
31+
*states_fields,
32+
*privacy_fields,
33+
)
34+
35+
def __init__(self, *args, **kwargs):
36+
super().__init__(*args, **kwargs)
37+
38+
# TODO: remove when this field is no-nullable
39+
self.fields['hidden'].widget = forms.CheckboxInput()
40+
self.fields['hidden'].empty_value = False
41+
42+
self.helper = FormHelper()
43+
self.helper.layout = Layout(
44+
Fieldset(
45+
_('States'),
46+
HTML(render_to_string('projects/project_version_states_help_text.html')),
47+
*self.Meta.states_fields,
48+
),
49+
Fieldset(
50+
_('Privacy'),
51+
*self.Meta.privacy_fields,
52+
),
53+
HTML(render_to_string(
54+
'projects/project_version_submit.html',
55+
context={'version': self.instance},
56+
)),
57+
)
2658

2759
def clean_active(self):
2860
active = self.cleaned_data['active']
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.11 on 2020-03-18 01:47
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('builds', '0017_builds_deterministic_order_index'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='version',
15+
name='hidden',
16+
field=models.BooleanField(null=True, default=False, help_text='Hide this version from the version (flyout) menu and search results?', verbose_name='Hidden'),
17+
),
18+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 2.2.11 on 2020-03-18 18:27
2+
3+
from django.db import migrations
4+
5+
6+
def forwards_func(apps, schema_editor):
7+
"""Migrate all protected versions to be hidden."""
8+
Version = apps.get_model('builds', 'Version')
9+
Version.objects.filter(privacy_level='protected').update(hidden=True)
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
('builds', '0018_add_hidden_field_to_version'),
16+
]
17+
18+
operations = [
19+
migrations.RunPython(forwards_func),
20+
]

readthedocs/builds/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ class Version(models.Model):
136136
default=settings.DEFAULT_VERSION_PRIVACY_LEVEL,
137137
help_text=_('Level of privacy for this Version.'),
138138
)
139+
hidden = models.BooleanField(
140+
_('Hidden'),
141+
# To avoid downtime during deploy, remove later.
142+
null=True,
143+
default=False,
144+
help_text=_('Hide this version from the version (flyout) menu and search results?')
145+
)
139146
machine = models.BooleanField(_('Machine Created'), default=False)
140147

141148
# Whether the latest successful build for this version contains certain media types

readthedocs/builds/querysets.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ def _add_user_repos(self, queryset, user):
2424
queryset = user_queryset | queryset
2525
return queryset
2626

27-
def public(self, user=None, project=None, only_active=True):
27+
def public(self, user=None, project=None, only_active=True, include_hidden=True):
2828
queryset = self.filter(privacy_level=constants.PUBLIC)
2929
if user:
3030
queryset = self._add_user_repos(queryset, user)
3131
if project:
3232
queryset = queryset.filter(project=project)
3333
if only_active:
3434
queryset = queryset.filter(active=True)
35+
if not include_hidden:
36+
queryset = queryset.filter(hidden=False)
3537
return queryset.distinct()
3638

3739
def protected(self, user=None, project=None, only_active=True):

readthedocs/proxito/tests/test_full.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import django_dynamic_fixture as fixture
77
from django.conf import settings
8+
from textwrap import dedent
89
from django.core.cache import cache
910
from django.http import HttpResponse
1011
from django.test.utils import override_settings
@@ -18,6 +19,8 @@
1819
SPHINX,
1920
SPHINX_HTMLDIR,
2021
SPHINX_SINGLEHTML,
22+
PUBLIC,
23+
PRIVATE,
2124
)
2225
from readthedocs.projects.models import Project, Domain
2326
from readthedocs.rtd_tests.storage import BuildMediaFileSystemStorageTest
@@ -285,10 +288,71 @@ def test_default_robots_txt(self, storage_exists):
285288
HTTP_HOST='project.readthedocs.io',
286289
)
287290
self.assertEqual(response.status_code, 200)
288-
self.assertEqual(
289-
response.content,
290-
b'User-agent: *\nAllow: /\nSitemap: https://project.readthedocs.io/sitemap.xml\n'
291+
expected = dedent(
292+
"""
293+
User-agent: *
294+
295+
Disallow: # Allow everything
296+
297+
Sitemap: https://project.readthedocs.io/sitemap.xml
298+
"""
299+
).lstrip()
300+
self.assertEqual(response.content.decode(), expected)
301+
302+
@mock.patch.object(BuildMediaFileSystemStorageTest, 'exists')
303+
def test_default_robots_txt_disallow_hidden_versions(self, storage_exists):
304+
storage_exists.return_value = False
305+
self.project.versions.update(active=True, built=True)
306+
fixture.get(
307+
Version,
308+
project=self.project,
309+
slug='hidden',
310+
active=True,
311+
hidden=True,
312+
privacy_level=PUBLIC,
313+
)
314+
fixture.get(
315+
Version,
316+
project=self.project,
317+
slug='hidden-2',
318+
active=True,
319+
hidden=True,
320+
privacy_level=PUBLIC,
291321
)
322+
fixture.get(
323+
Version,
324+
project=self.project,
325+
slug='hidden-and-inactive',
326+
active=False,
327+
hidden=True,
328+
privacy_level=PUBLIC,
329+
)
330+
fixture.get(
331+
Version,
332+
project=self.project,
333+
slug='hidden-and-private',
334+
active=False,
335+
hidden=True,
336+
privacy_level=PRIVATE,
337+
)
338+
339+
response = self.client.get(
340+
reverse('robots_txt'),
341+
HTTP_HOST='project.readthedocs.io',
342+
)
343+
self.assertEqual(response.status_code, 200)
344+
expected = dedent(
345+
"""
346+
User-agent: *
347+
348+
Disallow: /en/hidden-2/ # Hidden version
349+
350+
Disallow: /en/hidden/ # Hidden version
351+
352+
Sitemap: https://project.readthedocs.io/sitemap.xml
353+
"""
354+
).lstrip()
355+
self.assertEqual(response.content.decode(), expected)
292356

293357
@mock.patch.object(BuildMediaFileSystemStorageTest, 'exists')
294358
def test_default_robots_txt_private_version(self, storage_exists):

0 commit comments

Comments
 (0)