diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 595622f5b9c..8d107937854 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -279,7 +279,7 @@ Project details }, "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false, + "versioning_scheme": "multiple_versions_with_translations", "_links": { "_self": "/api/v3/projects/pip/", "versions": "/api/v3/projects/pip/versions/", @@ -295,6 +295,13 @@ Project details Allowed values are ``active_versions``, ``active_versions.last_build`` and ``active_versions.last_build.config``. Multiple fields can be passed separated by commas. + .. note:: + + ``versioning_scheme`` can be one of the following values: + + - ``multiple_versions_with_translations`` + - ``single_version_without_translations`` + .. note:: .. FIXME: we can't use :query string: here because it doesn't render properly @@ -305,6 +312,11 @@ Project details * **expand** (*string*) -- with ``organization`` and ``teams``. + .. note:: + + The ``single_version`` attribute is deprecated, + use ``versioning_scheme`` instead. + Project create ++++++++++++++ @@ -434,7 +446,7 @@ Project update "default_branch": "develop", "analytics_code": "UA000000", "analytics_disabled": false, - "single_version": false, + "versioning_scheme": "multiple_versions_with_translations", "external_builds_enabled": true, "privacy_level": "public", "external_builds_privacy_level": "public" diff --git a/docs/user/single-version.rst b/docs/user/single-version.rst index 521511ba5b4..66f3874d7c3 100644 --- a/docs/user/single-version.rst +++ b/docs/user/single-version.rst @@ -20,7 +20,7 @@ You can see a live example of this at http://www.contribution-guide.org Enabling ~~~~~~~~ -In your project's :guilabel:`Admin` page, you can toggle the :guilabel:`Single version` option on or off for your project . +In your project's :guilabel:`Admin` page, you can change the :guilabel:`Versioning scheme` option to :guilabel:`Single version without translations`. Check your :term:`dashboard` for a list of your projects. Effects diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index a526c97191e..cab929f874a 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -570,7 +570,7 @@ class Meta: "analytics_code", "analytics_disabled", "show_version_warning", - "single_version", + "versioning_scheme", "external_builds_enabled", "privacy_level", "external_builds_privacy_level", @@ -612,6 +612,7 @@ class ProjectSerializer(FlexFieldsModelSerializer): default_branch = serializers.CharField(source="get_default_branch") tags = serializers.StringRelatedField(many=True) users = UserSerializer(many=True) + single_version = serializers.BooleanField(source="is_single_version") _links = ProjectLinksSerializer(source="*") @@ -640,6 +641,9 @@ class Meta: "tags", "privacy_level", "external_builds_privacy_level", + "versioning_scheme", + # Kept for backwards compatibility, + # versioning_scheme should be used instead. "single_version", # NOTE: ``expandable_fields`` must not be included here. Otherwise, # they will be tried to be rendered and fail diff --git a/readthedocs/api/v3/tests/responses/projects-detail.json b/readthedocs/api/v3/tests/responses/projects-detail.json index bfeb346917a..1b21a984bd2 100644 --- a/readthedocs/api/v3/tests/responses/projects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-detail.json @@ -108,5 +108,6 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } diff --git a/readthedocs/api/v3/tests/responses/projects-list.json b/readthedocs/api/v3/tests/responses/projects-list.json index 216d37d235d..a4dceb1373e 100644 --- a/readthedocs/api/v3/tests/responses/projects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-list.json @@ -54,7 +54,8 @@ }, "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } ] } diff --git a/readthedocs/api/v3/tests/responses/projects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-list_POST.json index 7eb2ac36db4..54986791ee6 100644 --- a/readthedocs/api/v3/tests/responses/projects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-list_POST.json @@ -45,5 +45,6 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json index 46e9a40b989..ca2d7810937 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json @@ -50,6 +50,7 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } } diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json index ceaf2dca448..21ce2bb54a9 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json @@ -55,7 +55,8 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } } ] diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json index b8be0702756..d3ebd244a38 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json @@ -50,6 +50,7 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } } diff --git a/readthedocs/api/v3/tests/responses/projects-superproject.json b/readthedocs/api/v3/tests/responses/projects-superproject.json index 03fc35847e2..b2f9d3806fa 100644 --- a/readthedocs/api/v3/tests/responses/projects-superproject.json +++ b/readthedocs/api/v3/tests/responses/projects-superproject.json @@ -49,5 +49,6 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" } diff --git a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json index 2bbe17b8899..987d72de209 100644 --- a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json @@ -75,7 +75,8 @@ ], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" }, "triggered": true, "version": { diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 55762d7f782..a6b7d6721fb 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -50,7 +50,8 @@ "users": [{ "username": "testuser" }], "privacy_level": "public", "external_builds_privacy_level": "public", - "single_version": false + "single_version": false, + "versioning_scheme": "multiple_versions_with_translations" }], "remote_organization": { "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", diff --git a/readthedocs/api/v3/tests/test_projects.py b/readthedocs/api/v3/tests/test_projects.py index 4005814fd1b..a8df902b569 100644 --- a/readthedocs/api/v3/tests/test_projects.py +++ b/readthedocs/api/v3/tests/test_projects.py @@ -5,6 +5,7 @@ from django.urls import reverse from readthedocs.oauth.models import RemoteRepository +from readthedocs.projects.constants import SINGLE_VERSION_WITHOUT_TRANSLATIONS from readthedocs.projects.models import Project from .mixins import APIEndpointMixin @@ -404,7 +405,7 @@ def test_update_project(self): "default_branch": "updated-default-branch", "analytics_code": "UA-XXXXXX", "show_version_warning": False, - "single_version": True, + "versioning_scheme": SINGLE_VERSION_WITHOUT_TRANSLATIONS, "external_builds_enabled": True, } url = reverse( @@ -437,7 +438,7 @@ def test_update_project(self): self.assertEqual(self.project.default_branch, "updated-default-branch") self.assertEqual(self.project.analytics_code, "UA-XXXXXX") self.assertEqual(self.project.show_version_warning, False) - self.assertEqual(self.project.single_version, True) + self.assertEqual(self.project.is_single_version, True) self.assertEqual(self.project.external_builds_enabled, True) def test_partial_update_project(self): diff --git a/readthedocs/core/resolver.py b/readthedocs/core/resolver.py index f2b71afe1da..306e87c6746 100644 --- a/readthedocs/core/resolver.py +++ b/readthedocs/core/resolver.py @@ -111,7 +111,7 @@ def resolve_path( filename = self._fix_filename(filename) parent_project, project_relationship = self._get_canonical_project(project) - single_version = bool(project.single_version or single_version) + single_version = bool(project.is_single_version or single_version) # If the project is a subproject, we use the custom prefix # of the child of the relationship, this is since the project diff --git a/readthedocs/core/unresolver.py b/readthedocs/core/unresolver.py index 44b4f42de3d..376bd4ac322 100644 --- a/readthedocs/core/unresolver.py +++ b/readthedocs/core/unresolver.py @@ -443,7 +443,7 @@ def _unresolve_path_with_parent_project( :returns: A tuple with: project, version, and filename. """ # Multiversion project. - if not parent_project.single_version: + if not parent_project.is_single_version: response = self._match_multiversion_project( parent_project=parent_project, path=path, @@ -463,7 +463,7 @@ def _unresolve_path_with_parent_project( return response # Single version project. - if parent_project.single_version: + if parent_project.is_single_version: response = self._match_single_version_project( parent_project=parent_project, path=path, diff --git a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl index 6c74eef58a3..a3a4f0f946d 100644 --- a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl +++ b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl @@ -77,7 +77,7 @@ context = { 'programming_language': u'{{ project.programming_language }}', 'canonical_url': '{{ project.get_canonical_url }}', 'analytics_code': '{{ project.analytics_code }}', - 'single_version': {{ project.single_version }}, + 'single_version': {{ project.is_single_version }}, 'conf_py_path': '{{ conf_py_path }}', 'api_host': '{{ api_host }}', 'github_user': '{{ github_user }}', diff --git a/readthedocs/projects/constants.py b/readthedocs/projects/constants.py index 7fcb1cb1f90..7ffdfe649c2 100644 --- a/readthedocs/projects/constants.py +++ b/readthedocs/projects/constants.py @@ -383,43 +383,46 @@ 'https://bitbucket.org/{user}/{repo}/' 'src/{version}{docroot}{path}{source_suffix}' ) -BITBUCKET_COMMIT_URL = ( - 'https://bitbucket.org/{user}/{repo}/' - 'commits/{commit}' -) +BITBUCKET_COMMIT_URL = "https://bitbucket.org/{user}/{repo}/commits/{commit}" GITLAB_URL = ( - 'https://gitlab.com/{user}/{repo}/' - '{action}/{version}{docroot}{path}{source_suffix}' -) -GITLAB_COMMIT_URL = ( - 'https://gitlab.com/{user}/{repo}/' - 'commit/{commit}' + "https://gitlab.com/{user}/{repo}/" + "{action}/{version}{docroot}{path}{source_suffix}" ) +GITLAB_COMMIT_URL = "https://gitlab.com/{user}/{repo}/commit/{commit}" GITLAB_MERGE_REQUEST_COMMIT_URL = ( - 'https://gitlab.com/{user}/{repo}/' - 'commit/{commit}?merge_request_iid={number}' -) -GITLAB_MERGE_REQUEST_URL = ( - 'https://gitlab.com/{user}/{repo}/' - 'merge_requests/{number}' + "https://gitlab.com/{user}/{repo}/commit/{commit}?merge_request_iid={number}" ) +GITLAB_MERGE_REQUEST_URL = "https://gitlab.com/{user}/{repo}/merge_requests/{number}" # Patterns to pull merge/pull request from providers -GITHUB_PR_PULL_PATTERN = 'pull/{id}/head:external-{id}' -GITLAB_MR_PULL_PATTERN = 'merge-requests/{id}/head:external-{id}' +GITHUB_PR_PULL_PATTERN = "pull/{id}/head:external-{id}" +GITLAB_MR_PULL_PATTERN = "merge-requests/{id}/head:external-{id}" # Git provider names -GITHUB_BRAND = 'GitHub' -GITLAB_BRAND = 'GitLab' +GITHUB_BRAND = "GitHub" +GITLAB_BRAND = "GitLab" # SSL statuses -SSL_STATUS_VALID = 'valid' -SSL_STATUS_INVALID = 'invalid' -SSL_STATUS_PENDING = 'pending' -SSL_STATUS_UNKNOWN = 'unknown' +SSL_STATUS_VALID = "valid" +SSL_STATUS_INVALID = "invalid" +SSL_STATUS_PENDING = "pending" +SSL_STATUS_UNKNOWN = "unknown" SSL_STATUS_CHOICES = ( - (SSL_STATUS_VALID, _('Valid and active')), - (SSL_STATUS_INVALID, _('Invalid')), - (SSL_STATUS_PENDING, _('Pending')), - (SSL_STATUS_UNKNOWN, _('Unknown')), + (SSL_STATUS_VALID, _("Valid and active")), + (SSL_STATUS_INVALID, _("Invalid")), + (SSL_STATUS_PENDING, _("Pending")), + (SSL_STATUS_UNKNOWN, _("Unknown")), +) + +MULTIPLE_VERSIONS_WITH_TRANSLATIONS = "multiple_versions_with_translations" +SINGLE_VERSION_WITHOUT_TRANSLATIONS = "single_version_without_translations" +VERSIONING_SCHEME_CHOICES = ( + ( + MULTIPLE_VERSIONS_WITH_TRANSLATIONS, + _("Multiple versions with translations (///)"), + ), + ( + SINGLE_VERSION_WITHOUT_TRANSLATIONS, + _("Single version without translations (/)"), + ), ) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index ecdd29f16d5..9bdb8cd6e9d 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -209,7 +209,7 @@ class Meta: "analytics_code", "analytics_disabled", "show_version_warning", - "single_version", + "versioning_scheme", "external_builds_enabled", "external_builds_privacy_level", "readthedocs_yaml_path", @@ -236,6 +236,13 @@ def __init__(self, *args, **kwargs): self.fields['analytics_disabled'].widget = forms.CheckboxInput() self.fields['analytics_disabled'].empty_value = False + # Remove empty choice from options, and add a preview to the choices. + self.fields["versioning_scheme"].choices = [ + (key, value) + for key, value in self.fields["versioning_scheme"].choices + if key + ] + self.helper = FormHelper() help_text = render_to_string( 'projects/project_advanced_settings_helptext.html' diff --git a/readthedocs/projects/migrations/0109_add_project_versioning_scheme.py b/readthedocs/projects/migrations/0109_add_project_versioning_scheme.py new file mode 100644 index 00000000000..676d1cec190 --- /dev/null +++ b/readthedocs/projects/migrations/0109_add_project_versioning_scheme.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.5 on 2023-10-18 22:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0108_migrate_language_code"), + ] + + operations = [ + migrations.AddField( + model_name="historicalproject", + name="versioning_scheme", + field=models.CharField( + choices=[ + ( + "multiple_versions_with_translations", + "Multiple versions with translations (///)", + ), + ( + "single_version_without_translations", + "Single version without translations (/)", + ), + ], + default="multiple_versions_with_translations", + help_text="This affects how the URL of your documentation looks like, and if it supports translations or multiple versions. Changing the versioning scheme will break your current URLs.", + max_length=120, + null=True, + verbose_name="Versioning scheme", + ), + ), + migrations.AddField( + model_name="project", + name="versioning_scheme", + field=models.CharField( + choices=[ + ( + "multiple_versions_with_translations", + "Multiple versions with translations (///)", + ), + ( + "single_version_without_translations", + "Single version without translations (/)", + ), + ], + default="multiple_versions_with_translations", + help_text="This affects how the URL of your documentation looks like, and if it supports translations or multiple versions. Changing the versioning scheme will break your current URLs.", + max_length=120, + null=True, + verbose_name="Versioning scheme", + ), + ), + ] diff --git a/readthedocs/projects/migrations/0110_migrate_versioning_scheme.py b/readthedocs/projects/migrations/0110_migrate_versioning_scheme.py new file mode 100644 index 00000000000..683777a82f2 --- /dev/null +++ b/readthedocs/projects/migrations/0110_migrate_versioning_scheme.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.5 on 2023-10-18 22:39 + +from django.db import migrations + + +def forwards_func(apps, schema_editor): + """Migrate single version projects to new versioning scheme field.""" + Project = apps.get_model("projects", "Project") + Project.objects.filter(single_version=True).update( + versioning_scheme="single_version_without_translations", + # Set this field to false, so we always rely on the versioning scheme field instead. + single_version=False, + ) + # Migrate projects that were created before the versioning scheme field was added. + Project.objects.filter(versioning_scheme=None).update( + versioning_scheme="multiple_versions_with_translations" + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0109_add_project_versioning_scheme"), + ] + + operations = [ + migrations.RunPython(forwards_func), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 9f970a08a86..6a01a9aa57e 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -241,6 +241,20 @@ class Project(models.Model): blank=True, help_text=_('URL that documentation is expected to serve from'), ) + versioning_scheme = models.CharField( + _("Versioning scheme"), + max_length=120, + default=constants.MULTIPLE_VERSIONS_WITH_TRANSLATIONS, + choices=constants.VERSIONING_SCHEME_CHOICES, + # TODO: remove after migration + null=True, + help_text=_( + "This affects how the URL of your documentation looks like, " + "and if it supports translations or multiple versions. " + "Changing the versioning scheme will break your current URLs." + ), + ) + # TODO: this field is deprecated, use `versioning_scheme` instead. single_version = models.BooleanField( _('Single version'), default=False, @@ -843,6 +857,17 @@ def alias(self): if self.is_subproject: return self.superprojects.first().alias + @property + def is_single_version(self): + """ + Return whether or not this project is a single version without translations. + + Kept for backwards compatibility while we migrate the old field to the new one. + """ + if self.single_version: + return True + return self.versioning_scheme == constants.SINGLE_VERSION_WITHOUT_TRANSLATIONS + def subdomain(self, use_canonical_domain=True): """Get project subdomain from resolver.""" return Resolver().get_domain_without_protocol( diff --git a/readthedocs/projects/validators.py b/readthedocs/projects/validators.py index 06afa689dc3..06d8241660d 100644 --- a/readthedocs/projects/validators.py +++ b/readthedocs/projects/validators.py @@ -202,7 +202,7 @@ def validate_custom_subproject_prefix(project, prefix): # Since this will result in an ambiguous path that can't be resolved as a subproject. # This check is only needed if the project is a multiversion project, # a single version project will resolve the subproject correctly. - if not project.single_version and prefix.startswith(project_prefix): + if not project.is_single_version and prefix.startswith(project_prefix): first_component = prefix.removeprefix(project_prefix).split("/")[0] valid_languages = [language[0] for language in LANGUAGES] if first_component in valid_languages: diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json index 4049979aeef..c0ae375658f 100644 --- a/readthedocs/proxito/tests/responses/v0.json +++ b/readthedocs/proxito/tests/responses/v0.json @@ -25,6 +25,7 @@ "url": "https://github.com/readthedocs/project" }, "single_version": false, + "versioning_scheme": "multiple_versions_with_translations", "slug": "project", "subproject_of": null, "tags": ["project", "tag", "test"], diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 85e51188231..13562f5fa6d 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -252,7 +252,7 @@ def _v0(self, project, version, build, filename, url, user): version_downloads = [] versions_active_built_not_hidden = Version.objects.none() - if not project.single_version: + if not project.is_single_version: versions_active_built_not_hidden = ( Version.internal.public( project=project, diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index ad6999a9afe..27f212e5fea 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -269,7 +269,7 @@ def serve_path(self, request, path): # /pt-br/latest/pt_BR/index.html, but our protection for infinite redirects # will prevent a redirect loop. if ( - not project.single_version + not project.is_single_version and project.language in OLD_LANGUAGES_CODE_MAPPING and OLD_LANGUAGES_CODE_MAPPING[project.language] in path ): diff --git a/readthedocs/rtd_tests/tests/test_project_forms.py b/readthedocs/rtd_tests/tests/test_project_forms.py index 57707f2168d..9637abdf6b3 100644 --- a/readthedocs/rtd_tests/tests/test_project_forms.py +++ b/readthedocs/rtd_tests/tests/test_project_forms.py @@ -8,6 +8,7 @@ from readthedocs.builds.constants import EXTERNAL, LATEST, STABLE from readthedocs.builds.models import Version from readthedocs.projects.constants import ( + MULTIPLE_VERSIONS_WITH_TRANSLATIONS, PRIVATE, PUBLIC, REPO_TYPE_GIT, @@ -238,6 +239,7 @@ def test_cant_update_privacy_level(self): "documentation_type": SPHINX, "python_interpreter": "python3", "privacy_level": PRIVATE, + "versioning_scheme": MULTIPLE_VERSIONS_WITH_TRANSLATIONS, }, instance=self.project, ) @@ -254,6 +256,7 @@ def test_can_update_privacy_level(self): "python_interpreter": "python3", "privacy_level": PRIVATE, "external_builds_privacy_level": PRIVATE, + "versioning_scheme": MULTIPLE_VERSIONS_WITH_TRANSLATIONS, }, instance=self.project, ) @@ -271,6 +274,7 @@ def test_custom_readthedocs_yaml(self, update_docs_task): "python_interpreter": "python3", "privacy_level": PRIVATE, "readthedocs_yaml_path": custom_readthedocs_yaml_path, + "versioning_scheme": MULTIPLE_VERSIONS_WITH_TRANSLATIONS, }, instance=self.project, )