diff --git a/readthedocs/api/v2/serializers.py b/readthedocs/api/v2/serializers.py index a897cdc1403..8401266ca76 100644 --- a/readthedocs/api/v2/serializers.py +++ b/readthedocs/api/v2/serializers.py @@ -128,6 +128,7 @@ class VersionAdminSerializer(VersionSerializer): project = ProjectAdminSerializer() canonical_url = serializers.SerializerMethodField() build_data = serializers.JSONField(required=False, write_only=True, allow_null=True) + addons = serializers.BooleanField(required=False, write_only=True, allow_null=False) def get_canonical_url(self, obj): return obj.project.get_docs_url( @@ -138,6 +139,7 @@ def get_canonical_url(self, obj): class Meta(VersionSerializer.Meta): fields = VersionSerializer.Meta.fields + [ + "addons", "build_data", "canonical_url", ] diff --git a/readthedocs/builds/migrations/0051_add_addons_field.py b/readthedocs/builds/migrations/0051_add_addons_field.py new file mode 100644 index 00000000000..7b6aeb272ab --- /dev/null +++ b/readthedocs/builds/migrations/0051_add_addons_field.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.19 on 2023-05-23 07:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("builds", "0050_build_readthedocs_yaml_path"), + ] + + operations = [ + migrations.AddField( + model_name="version", + name="addons", + field=models.BooleanField( + blank=True, + default=False, + null=True, + verbose_name="Inject new addons js library for this version", + ), + ), + ] diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 3916e0e4c44..d8effa31c06 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -145,7 +145,11 @@ class Version(TimeStampedModel): populate_from='verbose_name', ) + # TODO: this field (`supported`) could be removed. It's returned only on + # the footer API response but I don't think anybody is using this field at + # all. supported = models.BooleanField(_('Supported'), default=True) + active = models.BooleanField(_('Active'), default=False) state = models.CharField( _("State"), @@ -156,7 +160,12 @@ class Version(TimeStampedModel): help_text=_("State of the PR/MR associated to this version."), ) built = models.BooleanField(_("Built"), default=False) + + # TODO: this field (`uploaded`) could be removed. It was used to mark a + # version as "Manually uploaded" by the core team, but this is not required + # anymore. Users can use `build.commands` for these cases now. uploaded = models.BooleanField(_("Uploaded"), default=False) + privacy_level = models.CharField( _('Privacy Level'), max_length=20, @@ -192,6 +201,13 @@ class Version(TimeStampedModel): null=True, ) + addons = models.BooleanField( + _("Inject new addons js library for this version"), + null=True, + blank=True, + default=False, + ) + objects = VersionManager.from_queryset(VersionQuerySet)() # Only include BRANCH, TAG, UNKNOWN type Versions. internal = InternalVersionManager.from_queryset(partial(VersionQuerySet, internal_only=True))() diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py index d49dd666464..e79cf17bcaf 100644 --- a/readthedocs/doc_builder/director.py +++ b/readthedocs/doc_builder/director.py @@ -59,6 +59,9 @@ def __init__(self, data): """ self.data = data + # Reset `addons` field. It will be set to `True` only when it's built via `build.commands` + self.data.version.addons = False + def setup_vcs(self): """ Perform all VCS related steps. @@ -422,6 +425,10 @@ def run_build_commands(self): # Update the `Version.documentation_type` to match the doctype defined # by the config file. When using `build.commands` it will be `GENERIC` self.data.version.documentation_type = self.data.config.doctype + + # Mark this version to inject the new js client when serving it via El Proxito + self.data.version.addons = True + self.store_readthedocs_build_yaml() def install_build_tools(self): diff --git a/readthedocs/projects/tasks/builds.py b/readthedocs/projects/tasks/builds.py index 38b357be785..55b00618351 100644 --- a/readthedocs/projects/tasks/builds.py +++ b/readthedocs/projects/tasks/builds.py @@ -600,6 +600,7 @@ def on_success(self, retval, task_id, args, kwargs): "has_epub": "epub" in valid_artifacts, "has_htmlzip": "htmlzip" in valid_artifacts, "build_data": self.data.version.build_data, + "addons": self.data.version.addons, } ) except HttpClientError: diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py index 670071c5111..3646d3f8652 100644 --- a/readthedocs/projects/tests/test_build_tasks.py +++ b/readthedocs/projects/tests/test_build_tasks.py @@ -299,6 +299,7 @@ def test_build_updates_documentation_type(self, load_yaml_config): assert self.requests_mock.request_history[7]._request.method == "PATCH" assert self.requests_mock.request_history[7].path == "/api/v2/version/1/" assert self.requests_mock.request_history[7].json() == { + "addons": False, "build_data": None, "built": True, "documentation_type": "mkdocs", @@ -552,6 +553,7 @@ def test_successful_build( assert self.requests_mock.request_history[7]._request.method == "PATCH" assert self.requests_mock.request_history[7].path == "/api/v2/version/1/" assert self.requests_mock.request_history[7].json() == { + "addons": False, "build_data": None, "built": True, "documentation_type": "sphinx", diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 85881d0fbe7..b8e93ea0366 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -289,10 +289,25 @@ def process_request(self, request): # noqa return None def add_hosting_integrations_headers(self, request, response): + addons = False project_slug = getattr(request, "path_project_slug", "") + version_slug = getattr(request, "path_version_slug", "") + if project_slug: project = Project.objects.get(slug=project_slug) + + # Check for the feature flag if project.has_feature(Feature.HOSTING_INTEGRATIONS): + addons = True + else: + # Check if the version forces injecting the addons (e.g. using `build.commands`) + version = ( + project.versions.filter(slug=version_slug).only("addons").first() + ) + if version and version.addons: + addons = True + + if addons: response["X-RTD-Hosting-Integrations"] = "true" def _get_https_redirect(self, request): diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py index c19bc817dd2..9cc2573f798 100644 --- a/readthedocs/proxito/tests/test_headers.py +++ b/readthedocs/proxito/tests/test_headers.py @@ -143,6 +143,18 @@ def test_user_domain_headers(self): self.assertEqual(r[http_header], http_header_value) self.assertEqual(r[http_header_secure], http_header_value) + def test_hosting_integrations_header(self): + version = self.project.versions.get(slug=LATEST) + version.addons = True + version.save() + + r = self.client.get( + "/en/latest/", secure=True, HTTP_HOST="project.dev.readthedocs.io" + ) + self.assertEqual(r.status_code, 200) + self.assertIsNotNone(r.get("X-RTD-Hosting-Integrations")) + self.assertEqual(r["X-RTD-Hosting-Integrations"], "true") + @override_settings(ALLOW_PRIVATE_REPOS=False) def test_cache_headers_public_version_with_private_projects_not_allowed(self): r = self.client.get(