Skip to content

Implement hidden state for versions #6792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1bebda0
Add tests
stsewd Mar 18, 2020
e60d997
Add hidden attribute
stsewd Mar 18, 2020
04ff029
Update form
stsewd Mar 18, 2020
645308d
Update queryset
stsewd Mar 18, 2020
526a023
Filter hidden versions from footer and search
stsewd Mar 18, 2020
46c59bf
Add hidden attribute to API v3
stsewd Mar 18, 2020
2326598
Update docs
stsewd Mar 18, 2020
8537331
Migrate protected versions to be hidden
stsewd Mar 18, 2020
6b04db1
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Mar 23, 2020
2768158
Apply suggestions from code review
stsewd Mar 24, 2020
c2b9350
Use include_hidden
stsewd Mar 24, 2020
055d862
Improve docs
stsewd Mar 24, 2020
ee486ba
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Mar 24, 2020
17a28eb
Improve docs
stsewd Mar 24, 2020
dfedc8b
Add guide
stsewd Mar 24, 2020
3bcf9fe
Try using crispy forms for better UX
stsewd Mar 24, 2020
5f3444f
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Mar 24, 2020
7058768
Fix migrations
stsewd Mar 24, 2020
60e12d5
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Mar 30, 2020
5303da4
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Apr 1, 2020
27f8311
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Apr 7, 2020
ea1f8ba
Apply suggestions from code review
stsewd Apr 20, 2020
2419877
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Apr 20, 2020
bc30c5e
Extension
stsewd Apr 20, 2020
a4c59ce
Fix migrations
stsewd Apr 20, 2020
649c90f
Avoid downtime
stsewd Apr 20, 2020
ced02ad
Fix form
stsewd Apr 20, 2020
a78d500
Merge branch 'master' into implement-hidden-state-for-versions
stsewd Apr 28, 2020
c69ad79
Update migrations
stsewd Apr 28, 2020
a85c1f1
Disallow hidden versions on robots.txt
stsewd Apr 28, 2020
bd476a7
Mention robots.txt in docs
stsewd Apr 28, 2020
a640499
Add comments to robots.txt
ericholscher Apr 28, 2020
965dfc0
Fix tests
ericholscher Apr 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/_static/images/guides/flyout-overwhelmed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ Version detail
"ref": "19.0.2",
"built": true,
"active": true,
"hidden": false,
"type": "tag",
"last_build": "{BUILD}",
"downloads": {
Expand Down Expand Up @@ -460,7 +461,8 @@ Version update
.. sourcecode:: json

{
"active": true
"active": true,
"hidden": false
}

:statuscode 204: Updated successfully
Expand Down
20 changes: 20 additions & 0 deletions docs/guides/hiding-a-version.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Hide a Version and Keep its Docs Online
=======================================

If you manage a project with a lot of versions,
the version (flyout) menu of your docs can be easily overwhelmed and hard to navigate.

.. figure:: /_static/images/guides/flyout-overwhelmed.png
:align: center

Overwhelmed flyout menu

You can deactivate the version to remove its docs,
but removing its docs isn't always an option.
To don't list a version in the flyout menu while keeping its docs online, you can mark it as hidden.
Go to the :guilabel:`Versions` tab of your project, click on :guilabel:`Edit` and mark the ``Hidden`` option.

Users that have a link to your old version will still be able to see your docs.
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/``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually want this? It doesn't make hidden very hidden, I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. However, if we hide the versions from there, you can't un-hide them. Although, we could show hidden versions in that list if you are an admin of the project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could show hidden versions in that list if you are an admin of the project.

Hmm, yeah. Not sure, I mean, users will only be able to get there by search results on google or old links, not from the project. I'll create an issue to discuss that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Check the docs about :ref:`versions' states <versions:States>` for more information.
3 changes: 2 additions & 1 deletion docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@ These guides will help you customize or tune aspects of Read the Docs.
autobuild-docs-for-pull-requests
build-notifications
build-using-too-many-resources
technical-docs-seo-guide
canonical
conda
environment-variables
feature-flags
google-analytics
hiding-a-version
searching-with-readthedocs
sitemaps
specifying-dependencies
technical-docs-seo-guide
wipe-environment


Expand Down
42 changes: 42 additions & 0 deletions docs/versions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,48 @@ they will be redirected to the **Default version**.
This defaults to **latest**,
but could also point to your latest released version.

States
------

States define the visibility of a version across the site.
You can change the states of a version from the :guilabel:`Versions` tab of your project.

Active
~~~~~~

- **Active**

- Docs for this version are visible
- Builds can be triggered for this version

- **Inactive**

- Docs for this version aren't visible
- Builds can't be triggered for this version

When you deactivate a version, its docs are removed.

Hidden
~~~~~~

- **Not hidden and Active**

- This version is listed on the version (flyout) menu on the docs site
- This version is shown in search results on the docs site

- **Hidden and Active**

- This version isn't listed on the version (flyout) menu on the docs site
- This version isn't show in search results from another version on the docs site
(like on search results from a superproject)

Hiding a version doesn't make it private,
any user with a link to its docs would be able to see it.
This is useful when:

- You no longer support a version, but you don't want to remove its docs.
- You have a work in progress version and don't want to publish its docs just yet.

Version warning
---------------

Expand Down
1 change: 1 addition & 0 deletions readthedocs/api/v2/views/footer_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def _get_active_versions_sorted(self):
project = self._get_project()
versions = project.ordered_active_versions(
user=self.request.user,
include_hidden=False,
)
return versions

Expand Down
1 change: 1 addition & 0 deletions readthedocs/api/v3/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class Meta:
'ref',
'built',
'active',
'hidden',
'type',
'downloads',
'urls',
Expand Down
1 change: 1 addition & 0 deletions readthedocs/api/v3/tests/responses/projects-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"active_versions": [
{
"active": true,
"hidden": false,
"built": true,
"downloads": {
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"triggered": true,
"version": {
"active": true,
"hidden": false,
"built": true,
"downloads": {
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"active": true,
"hidden": false,
"built": true,
"downloads": {
"epub": "https://project.readthedocs.io/_/downloads/en/v1.0/epub/",
Expand Down
30 changes: 29 additions & 1 deletion readthedocs/builds/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import re
import textwrap

from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Fieldset, Layout
from django import forms
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _

from readthedocs.builds.constants import (
Expand All @@ -22,7 +25,32 @@ class VersionForm(HideProtectedLevelMixin, forms.ModelForm):

class Meta:
model = Version
fields = ['active', 'privacy_level']
states_fields = ['active', 'hidden']
privacy_fields = ['privacy_level']
fields = (
*states_fields,
*privacy_fields,
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
_('States'),
HTML(render_to_string('projects/project_version_states_help_text.html')),
*self.Meta.states_fields,
),
Fieldset(
_('Privacy'),
*self.Meta.privacy_fields,
),
HTML(render_to_string(
'projects/project_version_submit.html',
context={'version': self.instance},
)),
)

def clean_active(self):
active = self.cleaned_data['active']
Expand Down
18 changes: 18 additions & 0 deletions readthedocs/builds/migrations/0016_add_hidden_field_to_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.11 on 2020-03-18 01:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('builds', '0015_uploading_build_state'),
]

operations = [
migrations.AddField(
model_name='version',
name='hidden',
field=models.BooleanField(default=False, help_text='Hide this version from the version (flyout) menu and search results?', verbose_name='Hidden'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 2.2.11 on 2020-03-18 18:27

from django.db import migrations


def forwards_func(apps, schema_editor):
"""Migrate all protected versions to be hidden."""
Version = apps.get_model('builds', 'Version')
Version.objects.filter(privacy_level='protected').update(hidden=True)


class Migration(migrations.Migration):

dependencies = [
('builds', '0016_add_hidden_field_to_version'),
]

operations = [
migrations.RunPython(forwards_func),
]
5 changes: 5 additions & 0 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ class Version(models.Model):
default=settings.DEFAULT_VERSION_PRIVACY_LEVEL,
help_text=_('Level of privacy for this Version.'),
)
hidden = models.BooleanField(
_('Hidden'),
default=False,
help_text=_('Hide this version from the version (flyout) menu and search results?')
)
machine = models.BooleanField(_('Machine Created'), default=False)

# Whether the latest successful build for this version contains certain media types
Expand Down
4 changes: 3 additions & 1 deletion readthedocs/builds/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ def _add_user_repos(self, queryset, user):
queryset = user_queryset | queryset
return queryset

def public(self, user=None, project=None, only_active=True):
def public(self, user=None, project=None, only_active=True, include_hidden=True):
queryset = self.filter(privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(project=project)
if only_active:
queryset = queryset.filter(active=True)
if not include_hidden:
queryset = queryset.filter(hidden=False)
return queryset.distinct()

def protected(self, user=None, project=None, only_active=True):
Expand Down
27 changes: 27 additions & 0 deletions readthedocs/rtd_tests/tests/test_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,33 @@ def test_index_pages_sphinx_htmldir(self):
self.assertNotIn('/en/latest/foo/index/bar.html', response.data['html'])
self.assertNotIn('/en/latest/foo/index/bar/index.html', response.data['html'])

def test_hidden_versions(self):
hidden_version = get(
Version,
slug='2.0',
hidden=True,
privacy_level=PUBLIC,
project=self.pip,
)

# The hidden version doesn't appear on the footer
self.url = (
reverse('footer_html') +
f'?project={self.pip.slug}&version={self.latest.slug}&page=index&docroot=/'
)
response = self.render()
self.assertIn('/en/latest/', response.data['html'])
self.assertNotIn('/en/2.0/', response.data['html'])

# We can access the hidden version, but it doesn't appear on the footer
self.url = (
reverse('footer_html') +
f'?project={self.pip.slug}&version={hidden_version.slug}&page=index&docroot=/'
)
response = self.render()
self.assertIn('/en/latest/', response.data['html'])
self.assertNotIn('/en/2.0/', response.data['html'])


class TestFooterHTML(BaseTestFooterHTML, TestCase):

Expand Down
7 changes: 4 additions & 3 deletions readthedocs/search/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,15 @@ def get_all_projects(self):
main_version = self._get_version()
main_project = self._get_project()

all_projects = [main_project]

subprojects = Project.objects.filter(
superprojects__parent_id=main_project.id,
)
all_projects = []
for project in list(subprojects) + [main_project]:
for project in subprojects:
version = (
Version.internal
.public(user=self.request.user, project=project)
.public(user=self.request.user, project=project, include_hidden=False)
.filter(slug=main_version.slug)
.first()
)
Expand Down
41 changes: 41 additions & 0 deletions readthedocs/search/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,47 @@ def test_get_all_projects_returns_empty_results(self, api_client, project):
data = resp.data['results']
assert len(data) == 0

def test_doc_search_hidden_versions(self, api_client, all_projects):
"""Test Document search return results from subprojects also"""
project = all_projects[0]
subproject = all_projects[1]
version = project.versions.all()[0]
# Add another project as subproject of the project
project.add_subproject(subproject)

version_subproject = subproject.versions.first()
version_subproject.hidden = True
version_subproject.save()

# Now search with subproject content but explicitly filter by the parent project
query = get_search_query_from_project_file(project_slug=subproject.slug)
search_params = {
'q': query,
'project': project.slug,
'version': version.slug
}
resp = self.get_search(api_client, search_params)
assert resp.status_code == 200

# The version from the subproject is hidden, so isn't show on the results.
data = resp.data['results']
assert len(data) == 0

# Now search on the subproject with hidden version
query = get_search_query_from_project_file(project_slug=subproject.slug)
search_params = {
'q': query,
'project': subproject.slug,
'version': version_subproject.slug
}
resp = self.get_search(api_client, search_params)
assert resp.status_code == 200
# We can still search inside the hidden version
data = resp.data['results']
assert len(data) == 1
first_result = data[0]
assert first_result['project'] == subproject.slug


class TestDocumentSearch(BaseTestDocumentSearch):

Expand Down
11 changes: 2 additions & 9 deletions readthedocs/templates/projects/project_version_detail.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "projects/base_project.html" %}

{% load crispy_forms_tags %}
{% load i18n %}
{% load privacy_tags %}

Expand Down Expand Up @@ -32,14 +33,6 @@ <h2> Editing {{ version.slug }} </h2>
{% endif %}
{% endif %}

{% crispy form %}

<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<p>
<input style="display: inline;" type="submit" value="{% trans "Save" %}">
{% trans "or" %}
<a href="{% url "wipe_version" version.project.slug version.slug %}">{% trans "wipe "%}</a>
</p>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}

<p>
{% blocktrans trimmed with docs_link="https://docs.readthedocs.io/page/versions.html#states" %}
Learn more about states <a href="{{ docs_link }}">here</a>.
{% endblocktrans %}
</p>
7 changes: 7 additions & 0 deletions readthedocs/templates/projects/project_version_submit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}

<p>
<input style="display: inline;" type="submit" value="{% trans "Save" %}">
{% trans "or" %}
<a href="{% url "wipe_version" version.project.slug version.slug %}">{% trans "wipe "%}</a>
</p>