diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 65600f8b0df..25c17695d7f 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -288,6 +288,7 @@ Project details "privacy_level": "public", "external_builds_privacy_level": "public", "versioning_scheme": "multiple_versions_with_translations", + "readthedocs_yaml_path": null, "_links": { "_self": "/api/v3/projects/pip/", "versions": "/api/v3/projects/pip/versions/", @@ -466,6 +467,7 @@ Project update "analytics_code": "UA000000", "analytics_disabled": false, "versioning_scheme": "multiple_versions_with_translations", + "readthedocs_yaml_path": "docs/.readthedocs.yaml", "external_builds_enabled": true, "privacy_level": "public", "external_builds_privacy_level": "public" diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index be78b46b8f0..b299b014b16 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -700,6 +700,7 @@ class Meta: "external_builds_enabled", "privacy_level", "external_builds_privacy_level", + "readthedocs_yaml_path", # NOTE: we do not allow to change any setting that can be set via # the YAML config file. ) @@ -794,6 +795,7 @@ class Meta: "urls", "tags", "privacy_level", + "readthedocs_yaml_path", "external_builds_privacy_level", "versioning_scheme", # Kept for backwards compatibility, diff --git a/readthedocs/api/v3/tests/responses/projects-detail.json b/readthedocs/api/v3/tests/responses/projects-detail.json index bc00dc35ad2..fdb34930988 100644 --- a/readthedocs/api/v3/tests/responses/projects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-detail.json @@ -73,6 +73,7 @@ "test" ], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/project/builds/", "documentation": "http://project.readthedocs.io/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/projects-list.json b/readthedocs/api/v3/tests/responses/projects-list.json index 38b8eb0e7eb..cffb5c7c0ff 100644 --- a/readthedocs/api/v3/tests/responses/projects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-list.json @@ -26,6 +26,7 @@ "default_branch": "master", "subproject_of": null, "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/project/builds/", "documentation": "http://project.readthedocs.io/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/projects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-list_POST.json index 2eec41c057d..1b8ab572497 100644 --- a/readthedocs/api/v3/tests/responses/projects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-list_POST.json @@ -34,6 +34,7 @@ "subproject_of": null, "tags": ["template tag", "test tag"], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/test-project/builds/", "documentation": "http://test-project.readthedocs.io/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json index 09140179130..55773e48429 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json @@ -39,6 +39,7 @@ "slug": "subproject", "tags": [], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/subproject/builds/", "documentation": "http://project.readthedocs.io/projects/subproject/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json index 3ba539a3881..f7c00933547 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json @@ -44,6 +44,7 @@ "slug": "subproject", "tags": [], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/subproject/builds/", "documentation": "http://project.readthedocs.io/projects/subproject/en/latest/", 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 66dea2e835f..b1753d66224 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json @@ -39,6 +39,7 @@ "slug": "new-project", "tags": [], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/new-project/builds/", "documentation": "http://project.readthedocs.io/projects/subproject-alias/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/projects-superproject.json b/readthedocs/api/v3/tests/responses/projects-superproject.json index 507f19bcff5..58a465e049c 100644 --- a/readthedocs/api/v3/tests/responses/projects-superproject.json +++ b/readthedocs/api/v3/tests/responses/projects-superproject.json @@ -38,6 +38,7 @@ "test" ], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/project/builds/", "documentation": "http://project.readthedocs.io/en/latest/", 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 47f2be8ae2e..fbda2e3a255 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 @@ -65,6 +65,7 @@ "test" ], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/project/builds/", "documentation": "http://project.readthedocs.io/en/latest/", diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 8733374a09e..eebe17576b4 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -43,6 +43,7 @@ "subproject_of": null, "tags": ["project", "tag", "test"], "translation_of": null, + "readthedocs_yaml_path": null, "urls": { "builds": "https://readthedocs.org/projects/project/builds/", "documentation": "http://project.readthedocs.io/en/latest/", diff --git a/readthedocs/api/v3/tests/test_projects.py b/readthedocs/api/v3/tests/test_projects.py index 3bde989e316..54634268d1f 100644 --- a/readthedocs/api/v3/tests/test_projects.py +++ b/readthedocs/api/v3/tests/test_projects.py @@ -661,6 +661,38 @@ def test_partial_update_project(self): self.assertEqual(list(self.project.tags.names()), ["partial tags", "updated"]) self.assertNotEqual(self.project.default_version, "updated-default-branch") + def test_partial_update_project_readthedocs_yaml_path(self): + """Test that readthedocs_yaml_path can be set via PATCH and is returned in GET.""" + url = reverse( + "projects-detail", + kwargs={ + "project_slug": self.project.slug, + }, + ) + + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + + # Verify the initial value is None + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIsNone(response.json()["readthedocs_yaml_path"]) + + # Set the readthedocs_yaml_path field + yaml_path = " docs/.readthedocs.yaml " + yaml_path_expected = "docs/.readthedocs.yaml" + data = {"readthedocs_yaml_path": yaml_path} + response = self.client.patch(url, data) + self.assertEqual(response.status_code, 204) + + # Verify it was saved to the database + self.project.refresh_from_db() + self.assertEqual(self.project.readthedocs_yaml_path, yaml_path_expected) + + # Verify it's returned in the GET response + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["readthedocs_yaml_path"], yaml_path_expected) + def test_partial_update_others_project(self): data = { "name": "Updated name", diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index fe584a0ad8e..27220c94e10 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -499,17 +499,6 @@ def __init__(self, *args, **kwargs): self.setup_external_builds_option() - def clean_readthedocs_yaml_path(self): - """ - Validate user input to help user. - - We also validate this path during the build process, so this validation step is - only considered as helpful to a user, not a security measure. - """ - filename = self.cleaned_data.get("readthedocs_yaml_path") - filename = (filename or "").strip() - return filename - def get_all_active_versions(self): """ Returns all active versions. diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index a40556df350..4ae7c91484f 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -49,6 +49,7 @@ from readthedocs.projects.querysets import FeatureQuerySet from readthedocs.projects.querysets import ProjectQuerySet from readthedocs.projects.querysets import RelatedProjectQuerySet +from readthedocs.projects.validators import normalize_readthedocs_yaml_path from readthedocs.projects.validators import validate_build_config_file from readthedocs.projects.validators import validate_custom_prefix from readthedocs.projects.validators import validate_custom_subproject_prefix @@ -685,6 +686,8 @@ def save(self, *args, **kwargs): if self.remote_repository and not dont_sync: self.repo = self.remote_repository.clone_url + self.readthedocs_yaml_path = normalize_readthedocs_yaml_path(self.readthedocs_yaml_path) + super().save(*args, **kwargs) self.update_latest_version() diff --git a/readthedocs/projects/validators.py b/readthedocs/projects/validators.py index 447f8e99a36..77c3fef77d5 100644 --- a/readthedocs/projects/validators.py +++ b/readthedocs/projects/validators.py @@ -243,3 +243,8 @@ def validate_environment_variable_size(project, new_env_value, error_class=Valid raise error_class( _("The total size of all environment variables in the project cannot exceed 256 KB.") ) + + +def normalize_readthedocs_yaml_path(value): + """Normalize user input for the path to ``.readthedocs.yaml``.""" + return (value or "").strip() diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index 1f704893f37..ac5f6e227dd 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -24,6 +24,7 @@ "type": "git", "url": "https://github.com/readthedocs/project" }, + "readthedocs_yaml_path": null, "single_version": false, "versioning_scheme": "multiple_versions_with_translations", "slug": "project",