From d320f2eade9697dfa35a12e86b84f1210f83e0d5 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 8 May 2023 19:27:39 -0500 Subject: [PATCH 01/10] API V3: clean version when deactivated and build version when activated Closes https://github.com/readthedocs/readthedocs.org/issues/10221. --- docs/user/api/v3.rst | 3 ++ readthedocs/api/v3/tests/test_versions.py | 52 ++++++++++++++++++- readthedocs/api/v3/views.py | 28 ++++++++++ readthedocs/builds/models.py | 11 ++++ readthedocs/projects/views/private.py | 11 +--- .../rtd_tests/tests/test_build_forms.py | 2 +- 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 4a0570467c9..8df10c609c5 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -580,6 +580,9 @@ Version update Update a version. + When a version is deactivated, its documentation is removed, + and when it's activated, a new build is triggered. + **Example request**: .. tabs:: diff --git a/readthedocs/api/v3/tests/test_versions.py b/readthedocs/api/v3/tests/test_versions.py index fc6c634571a..53499412d1e 100644 --- a/readthedocs/api/v3/tests/test_versions.py +++ b/readthedocs/api/v3/tests/test_versions.py @@ -1,5 +1,6 @@ import django_dynamic_fixture as fixture from django.test import override_settings +from unittest import mock from django.urls import reverse from readthedocs.builds.constants import EXTERNAL, TAG @@ -139,6 +140,7 @@ def test_projects_versions_detail_unique(self): ) self.assertEqual(response.status_code, 200) + @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources', new=mock.MagicMock) def test_projects_versions_partial_update(self): self.assertTrue(self.version.active) self.assertFalse(self.version.hidden) @@ -166,7 +168,7 @@ def test_projects_versions_partial_update(self): self.assertEqual(self.version.identifier, 'a1b2c3') self.assertFalse(self.version.active) self.assertTrue(self.version.hidden) - self.assertTrue(self.version.built) + self.assertFalse(self.version.built) self.assertEqual(self.version.type, TAG) def test_projects_versions_partial_update_privacy_levels_disabled(self): @@ -227,6 +229,54 @@ def test_projects_versions_partial_update_invalid_privacy_levels(self): self.assertEqual(response.status_code, 400) self.assertEqual(self.version.privacy_level, "public") + @mock.patch("readthedocs.api.v3.views.trigger_build") + @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') + def test_activate_version(self, clean_project_resources, trigger_build): + self.version.active = False + self.version.save() + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + self.assertFalse(self.version.active) + data = {"active": True} + response = self.client.patch( + reverse( + "projects-versions-detail", + kwargs={ + "parent_lookup_project__slug": self.project.slug, + "version_slug": self.version.slug, + }, + ), + data, + ) + self.assertEqual(response.status_code, 204) + self.version.refresh_from_db() + self.assertTrue(self.version.active) + clean_project_resources.assert_not_called() + trigger_build.assert_called_once() + + @mock.patch("readthedocs.api.v3.views.trigger_build") + @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') + def test_deactivate_version(self, clean_project_resources, trigger_build): + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + data = {"active": False} + self.assertTrue(self.version.active) + self.assertTrue(self.version.built) + response = self.client.patch( + reverse( + "projects-versions-detail", + kwargs={ + "parent_lookup_project__slug": self.project.slug, + "version_slug": self.version.slug, + }, + ), + data, + ) + self.assertEqual(response.status_code, 204) + self.version.refresh_from_db() + self.assertFalse(self.version.active) + self.assertFalse(self.version.built) + clean_project_resources.assert_called_once() + trigger_build.assert_not_called() + def test_projects_version_external(self): self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") self.version.type = EXTERNAL diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 652ecca5d05..00c598b2ae6 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,4 +1,5 @@ import django_filters.rest_framework as filters +from readthedocs.builds.signals import version_changed from django.db.models import Exists, OuterRef from rest_flex_fields import is_expanded from rest_flex_fields.views import FlexFieldsMixin @@ -297,6 +298,33 @@ def get_serializer_class(self): return VersionSerializer return VersionUpdateSerializer + def update(self, request, *args, **kwargs): + """ + Run extra steps after updating a version. + + - When a version is deactivated, we need to clean up its + files from storage, and search index. + - When a version is activated, we need to trigger a build. + - We also need to purge the cache from the CDN, + since the version could have been activated/deactivated, + or its privacy level could have changed. + """ + # Get the current value before updating. + version = self.get_object() + was_active = version.active + result = super().update(request, *args, **kwargs) + # Get the updated version. + version = self.get_object() + # If the version is deactivated, we need to clean up the files. + if was_active and not version.active: + version.clean_resources() + # If the version is activated, we need to trigger a build. + if not was_active and version.active: + trigger_build(project=version.project, version=version) + # Purge the cache from the CDN. + version_changed.send(sender=self.__class__, version=version) + return result + class BuildsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, FlexFieldsMixin, ReadOnlyModelViewSet): diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index c0c4ab8af83..cbf7f18227e 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -381,6 +381,17 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ clean_project_resources(self.project, self) super().delete(*args, **kwargs) + def clean_resources(self): + from readthedocs.projects.tasks.utils import clean_project_resources + log.info( + "Removing files for version.", + project_slug=self.project.slug, + version_slug=self.slug, + ) + clean_project_resources(project=self.project, version=self) + self.built = False + self.save() + @property def identifier_friendly(self): """Return display friendly identifier.""" diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index fece00c1cc4..38b55c9baa1 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -218,16 +218,7 @@ def form_valid(self, form): version = form.save() if form.has_changed(): if 'active' in form.changed_data and version.active is False: - log.info( - 'Removing files for version.', - version_slug=version.slug, - ) - clean_project_resources( - version.project, - version, - ) - version.built = False - version.save() + version.clean_resources() return HttpResponseRedirect(self.get_success_url()) diff --git a/readthedocs/rtd_tests/tests/test_build_forms.py b/readthedocs/rtd_tests/tests/test_build_forms.py index 4ad3077d56d..8ba979bc72a 100644 --- a/readthedocs/rtd_tests/tests/test_build_forms.py +++ b/readthedocs/rtd_tests/tests/test_build_forms.py @@ -91,7 +91,7 @@ def test_can_update_privacy_level(self): self.assertEqual(version.privacy_level, PRIVATE) @mock.patch('readthedocs.builds.forms.trigger_build', mock.MagicMock()) - @mock.patch('readthedocs.projects.views.private.clean_project_resources') + @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') def test_resources_are_deleted_when_version_is_inactive(self, clean_project_resources): version = get( Version, From 8ba0fc26c59651abf49446fc440fb4e7c2c1059a Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 8 May 2023 19:30:20 -0500 Subject: [PATCH 02/10] Docstring --- readthedocs/builds/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index cbf7f18227e..5ecf78c8376 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -382,6 +382,12 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ super().delete(*args, **kwargs) def clean_resources(self): + """ + Remove all resources from this version. + + This includes removing files from storage, + and removing its search index. + """ from readthedocs.projects.tasks.utils import clean_project_resources log.info( "Removing files for version.", From 09bde3fbd3921b562282a0f106f96156166f88f5 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 8 May 2023 19:32:50 -0500 Subject: [PATCH 03/10] Black --- readthedocs/api/v3/tests/test_versions.py | 11 +++++++---- readthedocs/api/v3/views.py | 2 +- readthedocs/builds/models.py | 1 + readthedocs/rtd_tests/tests/test_build_forms.py | 8 +++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/readthedocs/api/v3/tests/test_versions.py b/readthedocs/api/v3/tests/test_versions.py index 53499412d1e..b37201a9c6b 100644 --- a/readthedocs/api/v3/tests/test_versions.py +++ b/readthedocs/api/v3/tests/test_versions.py @@ -1,6 +1,7 @@ +from unittest import mock + import django_dynamic_fixture as fixture from django.test import override_settings -from unittest import mock from django.urls import reverse from readthedocs.builds.constants import EXTERNAL, TAG @@ -140,7 +141,9 @@ def test_projects_versions_detail_unique(self): ) self.assertEqual(response.status_code, 200) - @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources', new=mock.MagicMock) + @mock.patch( + "readthedocs.projects.tasks.utils.clean_project_resources", new=mock.MagicMock + ) def test_projects_versions_partial_update(self): self.assertTrue(self.version.active) self.assertFalse(self.version.hidden) @@ -230,7 +233,7 @@ def test_projects_versions_partial_update_invalid_privacy_levels(self): self.assertEqual(self.version.privacy_level, "public") @mock.patch("readthedocs.api.v3.views.trigger_build") - @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') + @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") def test_activate_version(self, clean_project_resources, trigger_build): self.version.active = False self.version.save() @@ -254,7 +257,7 @@ def test_activate_version(self, clean_project_resources, trigger_build): trigger_build.assert_called_once() @mock.patch("readthedocs.api.v3.views.trigger_build") - @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') + @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") def test_deactivate_version(self, clean_project_resources, trigger_build): self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") data = {"active": False} diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 00c598b2ae6..fffc0febfd7 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,5 +1,4 @@ import django_filters.rest_framework as filters -from readthedocs.builds.signals import version_changed from django.db.models import Exists, OuterRef from rest_flex_fields import is_expanded from rest_flex_fields.views import FlexFieldsMixin @@ -22,6 +21,7 @@ from rest_framework_extensions.mixins import NestedViewSetMixin from readthedocs.builds.models import Build, Version +from readthedocs.builds.signals import version_changed from readthedocs.core.utils import trigger_build from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.oauth.models import ( diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 5ecf78c8376..da6fbe278c3 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -389,6 +389,7 @@ def clean_resources(self): and removing its search index. """ from readthedocs.projects.tasks.utils import clean_project_resources + log.info( "Removing files for version.", project_slug=self.project.slug, diff --git a/readthedocs/rtd_tests/tests/test_build_forms.py b/readthedocs/rtd_tests/tests/test_build_forms.py index 8ba979bc72a..18710ff0505 100644 --- a/readthedocs/rtd_tests/tests/test_build_forms.py +++ b/readthedocs/rtd_tests/tests/test_build_forms.py @@ -90,9 +90,11 @@ def test_can_update_privacy_level(self): self.assertTrue(form.is_valid()) self.assertEqual(version.privacy_level, PRIVATE) - @mock.patch('readthedocs.builds.forms.trigger_build', mock.MagicMock()) - @mock.patch('readthedocs.projects.tasks.utils.clean_project_resources') - def test_resources_are_deleted_when_version_is_inactive(self, clean_project_resources): + @mock.patch("readthedocs.builds.forms.trigger_build", mock.MagicMock()) + @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") + def test_resources_are_deleted_when_version_is_inactive( + self, clean_project_resources + ): version = get( Version, project=self.project, From 18a0c2208427d7046ff510d06441e8992c11db7e Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 15 May 2023 17:58:24 -0500 Subject: [PATCH 04/10] Mention cache invalidation --- docs/user/api/v3.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 8df10c609c5..9d9a24fe53c 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -583,6 +583,8 @@ Version update When a version is deactivated, its documentation is removed, and when it's activated, a new build is triggered. + Updates to a version also invalidates its CDN cache. + **Example request**: .. tabs:: From 9e322a5c08a8ddd25987656e0df7ce43157a48e8 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 15 May 2023 19:10:07 -0500 Subject: [PATCH 05/10] Re-use same logic in form and API --- readthedocs/api/v3/views.py | 21 ++------------------- readthedocs/builds/forms.py | 11 +++++------ readthedocs/builds/models.py | 26 +++++++++++++++++++++++++- readthedocs/projects/views/private.py | 5 +---- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index fffc0febfd7..5b34c4f6fd3 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -21,7 +21,6 @@ from rest_framework_extensions.mixins import NestedViewSetMixin from readthedocs.builds.models import Build, Version -from readthedocs.builds.signals import version_changed from readthedocs.core.utils import trigger_build from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.oauth.models import ( @@ -299,30 +298,14 @@ def get_serializer_class(self): return VersionUpdateSerializer def update(self, request, *args, **kwargs): - """ - Run extra steps after updating a version. - - - When a version is deactivated, we need to clean up its - files from storage, and search index. - - When a version is activated, we need to trigger a build. - - We also need to purge the cache from the CDN, - since the version could have been activated/deactivated, - or its privacy level could have changed. - """ + """Overridden to call ``post_save`` method on the updated version.""" # Get the current value before updating. version = self.get_object() was_active = version.active result = super().update(request, *args, **kwargs) # Get the updated version. version = self.get_object() - # If the version is deactivated, we need to clean up the files. - if was_active and not version.active: - version.clean_resources() - # If the version is activated, we need to trigger a build. - if not was_active and version.active: - trigger_build(project=version.project, version=version) - # Purge the cache from the CDN. - version_changed.send(sender=self.__class__, version=version) + version.post_save(was_active=was_active) return result diff --git a/readthedocs/builds/forms.py b/readthedocs/builds/forms.py index d59939161cc..234384a4001 100644 --- a/readthedocs/builds/forms.py +++ b/readthedocs/builds/forms.py @@ -22,8 +22,6 @@ Version, VersionAutomationRule, ) -from readthedocs.builds.signals import version_changed -from readthedocs.core.utils import trigger_build class VersionForm(forms.ModelForm): @@ -85,11 +83,12 @@ def _is_default_version(self): return project.default_version == self.instance.slug def save(self, commit=True): + # If the version is created, it's not active yet. + was_active = False + if self.instance: + was_active = self.instance.active obj = super().save(commit=commit) - if obj.active and not obj.built and not obj.uploaded: - trigger_build(project=obj.project, version=obj) - if self.has_changed(): - version_changed.send(sender=self.__class__, version=obj) + obj.post_save(was_active=was_active) return obj diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index da6fbe278c3..79f90af13e6 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -1,5 +1,4 @@ """Models for the builds app.""" - import datetime import os.path import re @@ -52,6 +51,7 @@ RelatedBuildQuerySet, VersionQuerySet, ) +from readthedocs.builds.signals import version_changed from readthedocs.builds.utils import ( external_version_name, get_bitbucket_username_repo, @@ -61,6 +61,7 @@ ) from readthedocs.builds.version_slug import VersionSlugField from readthedocs.config import LATEST_CONFIGURATION_VERSION +from readthedocs.core.utils import trigger_build from readthedocs.projects.constants import ( BITBUCKET_COMMIT_URL, BITBUCKET_URL, @@ -399,6 +400,29 @@ def clean_resources(self): self.built = False self.save() + def post_save(self, was_active=False): + """ + Run extra steps after updating a version. + + Useful to run after the version has been saved/updated + by the user, like from a form or API. + + - When a version is deactivated, we need to clean up its + files from storage, and search index. + - When a version is activated, we need to trigger a build. + - We also need to purge the cache from the CDN, + since the version could have been activated/deactivated, + or its privacy level could have changed. + """ + # If the version is deactivated, we need to clean up the files. + if was_active and not self.active: + self.clean_resources() + # If the version is activated, we need to trigger a build. + if not was_active and self.active: + trigger_build(project=self.project, version=self) + # Purge the cache from the CDN. + version_changed.send(sender=self.__class__, version=self) + @property def identifier_friendly(self): """Return display friendly identifier.""" diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 38b55c9baa1..368798bd7c0 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -215,10 +215,7 @@ def get_form(self, data=None, files=None, **kwargs): return self.get_form_class()(data, files, **kwargs) def form_valid(self, form): - version = form.save() - if form.has_changed(): - if 'active' in form.changed_data and version.active is False: - version.clean_resources() + form.save() return HttpResponseRedirect(self.get_success_url()) From 928cbda496ff1551a90231122edb14e687dc0219 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 15 May 2023 19:16:29 -0500 Subject: [PATCH 06/10] Update tests --- readthedocs/api/v3/tests/test_versions.py | 4 ++-- readthedocs/rtd_tests/tests/test_build_forms.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/api/v3/tests/test_versions.py b/readthedocs/api/v3/tests/test_versions.py index b37201a9c6b..775cf8563b7 100644 --- a/readthedocs/api/v3/tests/test_versions.py +++ b/readthedocs/api/v3/tests/test_versions.py @@ -232,7 +232,7 @@ def test_projects_versions_partial_update_invalid_privacy_levels(self): self.assertEqual(response.status_code, 400) self.assertEqual(self.version.privacy_level, "public") - @mock.patch("readthedocs.api.v3.views.trigger_build") + @mock.patch("readthedocs.builds.models.trigger_build") @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") def test_activate_version(self, clean_project_resources, trigger_build): self.version.active = False @@ -256,7 +256,7 @@ def test_activate_version(self, clean_project_resources, trigger_build): clean_project_resources.assert_not_called() trigger_build.assert_called_once() - @mock.patch("readthedocs.api.v3.views.trigger_build") + @mock.patch("readthedocs.builds.models.trigger_build") @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") def test_deactivate_version(self, clean_project_resources, trigger_build): self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") diff --git a/readthedocs/rtd_tests/tests/test_build_forms.py b/readthedocs/rtd_tests/tests/test_build_forms.py index 18710ff0505..ad3b43530dd 100644 --- a/readthedocs/rtd_tests/tests/test_build_forms.py +++ b/readthedocs/rtd_tests/tests/test_build_forms.py @@ -90,7 +90,7 @@ def test_can_update_privacy_level(self): self.assertTrue(form.is_valid()) self.assertEqual(version.privacy_level, PRIVATE) - @mock.patch("readthedocs.builds.forms.trigger_build", mock.MagicMock()) + @mock.patch("readthedocs.builds.models.trigger_build", mock.MagicMock()) @mock.patch("readthedocs.projects.tasks.utils.clean_project_resources") def test_resources_are_deleted_when_version_is_inactive( self, clean_project_resources From 1e971a0a2d46a508eba480495bfcf76fccf2e015 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 15 May 2023 19:56:05 -0500 Subject: [PATCH 07/10] Fix --- readthedocs/builds/forms.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/readthedocs/builds/forms.py b/readthedocs/builds/forms.py index 234384a4001..90d1df426fe 100644 --- a/readthedocs/builds/forms.py +++ b/readthedocs/builds/forms.py @@ -65,6 +65,9 @@ def __init__(self, *args, **kwargs): self.helper = FormHelper() self.helper.layout = Layout(*field_sets) + # We need to know if the version was active before the update. + # We use this value in the save method. + self._was_active = self.instance.active if self.instance else False def clean_active(self): active = self.cleaned_data['active'] @@ -83,12 +86,8 @@ def _is_default_version(self): return project.default_version == self.instance.slug def save(self, commit=True): - # If the version is created, it's not active yet. - was_active = False - if self.instance: - was_active = self.instance.active obj = super().save(commit=commit) - obj.post_save(was_active=was_active) + obj.post_save(was_active=self._was_active) return obj From 6bb681e4c25ad16569fdc29a682bb4f15a11bd56 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Tue, 16 May 2023 13:40:23 -0500 Subject: [PATCH 08/10] Update readthedocs/builds/models.py Co-authored-by: Benjamin Balder Bach --- readthedocs/builds/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 79f90af13e6..7ebe0406bcb 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -403,6 +403,9 @@ def clean_resources(self): def post_save(self, was_active=False): """ Run extra steps after updating a version. + + This method isn't called automatically by a signal but is called explicitly + from other processes. Useful to run after the version has been saved/updated by the user, like from a form or API. From 301093c6c9d93d8a2d302286ac4c4397dcab2d77 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 17 May 2023 12:24:25 -0500 Subject: [PATCH 09/10] Fix linter --- readthedocs/builds/models.py | 4 ++-- readthedocs/rtd_tests/files/test.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 7ebe0406bcb..3916e0e4c44 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -403,8 +403,8 @@ def clean_resources(self): def post_save(self, was_active=False): """ Run extra steps after updating a version. - - This method isn't called automatically by a signal but is called explicitly + + This method isn't called automatically by a signal but is called explicitly from other processes. Useful to run after the version has been saved/updated diff --git a/readthedocs/rtd_tests/files/test.html b/readthedocs/rtd_tests/files/test.html index a7b1555f981..0aeac33d5b1 100644 --- a/readthedocs/rtd_tests/files/test.html +++ b/readthedocs/rtd_tests/files/test.html @@ -1 +1 @@ -Something Else +Something Else \ No newline at end of file From 214bbe09c1e91837fe6a7a8fd4840f320c31fd67 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 17 May 2023 12:28:43 -0500 Subject: [PATCH 10/10] Linter --- readthedocs/rtd_tests/files/test.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/rtd_tests/files/test.html b/readthedocs/rtd_tests/files/test.html index 0aeac33d5b1..a7b1555f981 100644 --- a/readthedocs/rtd_tests/files/test.html +++ b/readthedocs/rtd_tests/files/test.html @@ -1 +1 @@ -Something Else \ No newline at end of file +Something Else