From 7095954b630d0607f0288b806e2a1aeba0128b25 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 13 Nov 2024 12:41:27 +0100 Subject: [PATCH 1/6] Addons: add `addons.configs.load_when_embedded` in the API response Add an extra addons config to decide whether or not force the injection of addons when the page is embedded (eg. iframe). By default, we are not loading addons if embedded. Required by https://github.com/readthedocs/addons/pull/415 Closes https://github.com/readthedocs/addons/issues/412 --- .../0133_addons_load_when_embedded.py | 25 +++++++++++++++++++ .../0134_addons_load_when_embedded_notnull.py | 25 +++++++++++++++++++ readthedocs/projects/models.py | 4 +++ readthedocs/proxito/middleware.py | 9 +++---- readthedocs/proxito/tests/responses/v1.json | 3 +++ readthedocs/proxito/views/hosting.py | 3 +++ 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 readthedocs/projects/migrations/0133_addons_load_when_embedded.py create mode 100644 readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py diff --git a/readthedocs/projects/migrations/0133_addons_load_when_embedded.py b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py new file mode 100644 index 00000000000..fe3a4c0893b --- /dev/null +++ b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 12:00 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy + + dependencies = [ + ('projects', '0132_addons_linkpreviews_fields'), + ] + + operations = [ + migrations.AddField( + model_name='addonsconfig', + name='load_when_embedded', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='load_when_embedded', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py new file mode 100644 index 00000000000..7bad799c343 --- /dev/null +++ b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 12:01 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.after_deploy + + dependencies = [ + ('projects', '0133_addons_load_when_embedded'), + ] + + operations = [ + migrations.AlterField( + model_name='addonsconfig', + name='load_when_embedded', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='historicaladdonsconfig', + name='load_when_embedded', + field=models.BooleanField(default=False), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index de284079542..d4d655ff0cb 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -167,6 +167,10 @@ class AddonsConfig(TimeStampedModel): help_text="Enable/Disable all the addons on this project", ) + # Whether or not load addons library when the requested page is embedded (e.g. inside an iframe) + # https://github.com/readthedocs/addons/pull/415 + load_when_embedded = models.BooleanField(default=False) + # Analytics # NOTE: we keep analytics disabled by default to save resources. diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 5db6f24e5ca..772ce06a781 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -30,7 +30,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag -from readthedocs.projects.models import Project +from readthedocs.projects.models import AddonsConfig from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response from readthedocs.proxito.redirects import redirect_to_https @@ -283,12 +283,11 @@ def add_hosting_integrations_headers(self, request, response): project_slug = getattr(request, "path_project_slug", "") if project_slug: - addons = Project.objects.filter( - slug=project_slug, addons__enabled=True - ).exists() + addons = AddonsConfig.objects.filter(project__slug=project_slug).first() if addons: - response["X-RTD-Force-Addons"] = "true" + if addons.enabled: + response["X-RTD-Force-Addons"] = "true" def add_cors_headers(self, request, response): """ diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index 70e6fa26556..9739d2e614c 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -121,6 +121,9 @@ } }, "addons": { + "configs": { + "load_when_embedded": false + }, "analytics": { "enabled": false, "code": null diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 6ef3fb752d8..3df2c3d37f6 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -454,6 +454,9 @@ def _v1(self, project, version, build, filename, url, request): # Mainly, all the fields including a Project, Version or Build will use the exact same # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "addons": { + "configs": { + "load_when_embedded": project.addons.load_when_embedded, + }, "analytics": { "enabled": project.addons.analytics_enabled, # TODO: consider adding this field into the ProjectSerializer itself. From 774de8ec40a63a655da44fe42c1936857fdfd61c Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 14 Nov 2024 12:06:18 +0100 Subject: [PATCH 2/6] Rename `configs` with `options` --- .../projects/migrations/0133_addons_load_when_embedded.py | 4 ++-- .../migrations/0134_addons_load_when_embedded_notnull.py | 6 +++--- readthedocs/projects/models.py | 2 +- readthedocs/proxito/views/hosting.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/readthedocs/projects/migrations/0133_addons_load_when_embedded.py b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py index fe3a4c0893b..f261ec119f4 100644 --- a/readthedocs/projects/migrations/0133_addons_load_when_embedded.py +++ b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py @@ -14,12 +14,12 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='addonsconfig', - name='load_when_embedded', + name='options_load_when_embedded', field=models.BooleanField(default=False, null=True), ), migrations.AddField( model_name='historicaladdonsconfig', - name='load_when_embedded', + name='options_load_when_embedded', field=models.BooleanField(default=False, null=True), ), ] diff --git a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py index 7bad799c343..6481657f100 100644 --- a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py +++ b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py @@ -8,18 +8,18 @@ class Migration(migrations.Migration): safe = Safe.after_deploy dependencies = [ - ('projects', '0133_addons_load_when_embedded'), + ('projects', '0133_addons_options_load_when_embedded'), ] operations = [ migrations.AlterField( model_name='addonsconfig', - name='load_when_embedded', + name='options_load_when_embedded', field=models.BooleanField(default=False), ), migrations.AlterField( model_name='historicaladdonsconfig', - name='load_when_embedded', + name='options_load_when_embedded', field=models.BooleanField(default=False), ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index d4d655ff0cb..ae9e1250fc3 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -169,7 +169,7 @@ class AddonsConfig(TimeStampedModel): # Whether or not load addons library when the requested page is embedded (e.g. inside an iframe) # https://github.com/readthedocs/addons/pull/415 - load_when_embedded = models.BooleanField(default=False) + options_load_when_embedded = models.BooleanField(default=False) # Analytics diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 3df2c3d37f6..51bf9d60fbc 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -454,7 +454,7 @@ def _v1(self, project, version, build, filename, url, request): # Mainly, all the fields including a Project, Version or Build will use the exact same # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "addons": { - "configs": { + "options": { "load_when_embedded": project.addons.load_when_embedded, }, "analytics": { From 079283b3bd780acfc041bd06a6279609cd4ea4cd Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Nov 2024 16:46:52 +0100 Subject: [PATCH 3/6] Adddons: allow injecting an "always live" JavaScript file (#11758) We talked about giving users a way to inject a JavaScript file they control using our Cloudflare Worker infrastructure to allow them manipulate frozen documentations. This could be used in different ways to fix bugs or add features to a particular frozen set of docs or even to all the versions. The user can make usage of API data to filter by version or not (e.g. `if (versions.current == "v3.0") { .. do something ...} `) The script could live in Read the Docs itself using a relative URL, or outside it, using an absolute URL. ### Example using Sphinx 1. [Tell Sphinx to include `static` folder](https://github.com/readthedocs/test-builds/blob/full-feature/docs/conf.py#L25) 2. [Include a `readthedocs.js` file in your project](https://github.com/readthedocs/test-builds/blob/full-feature/docs/static/readthedocs.js) 3. Make `AddonsConfig.userjsfile_src` to be `/en/full-feature/readthedocs.js` from addons admin UX The console will render the following: ![Screenshot_2024-11-11_18-13-59](https://github.com/user-attachments/assets/9fa46525-3b20-4d88-9950-f0a93a1199ab) Related https://github.com/readthedocs/readthedocs.org/issues/11474 Related https://github.com/readthedocs/addons/pull/431 --- .../migrations/0135_addons_customscript.py | 35 +++++++++++++++++++ .../0136_addons_customscript_notnull.py | 25 +++++++++++++ readthedocs/projects/models.py | 12 +++++++ readthedocs/proxito/tests/responses/v1.json | 4 +++ readthedocs/proxito/views/hosting.py | 4 +++ 5 files changed, 80 insertions(+) create mode 100644 readthedocs/projects/migrations/0135_addons_customscript.py create mode 100644 readthedocs/projects/migrations/0136_addons_customscript_notnull.py diff --git a/readthedocs/projects/migrations/0135_addons_customscript.py b/readthedocs/projects/migrations/0135_addons_customscript.py new file mode 100644 index 00000000000..d82ab66cc0a --- /dev/null +++ b/readthedocs/projects/migrations/0135_addons_customscript.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2024-11-13 13:34 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy + + dependencies = [ + ('projects', '0134_addons_load_when_embedded_notnull'), + ] + + operations = [ + migrations.AddField( + model_name='addonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='addonsconfig', + name='customscript_src', + field=models.CharField(blank=True, help_text='URL to a JavaScript file to inject at serve time', max_length=512, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='customscript_src', + field=models.CharField(blank=True, help_text='URL to a JavaScript file to inject at serve time', max_length=512, null=True), + ), + ] diff --git a/readthedocs/projects/migrations/0136_addons_customscript_notnull.py b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py new file mode 100644 index 00000000000..18ea9b495ac --- /dev/null +++ b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 13:36 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.after_deploy + + dependencies = [ + ('projects', '0135_addons_customscript'), + ] + + operations = [ + migrations.AlterField( + model_name='addonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='historicaladdonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index ae9e1250fc3..b8358f0f7f9 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -222,6 +222,18 @@ class AddonsConfig(TimeStampedModel): search_enabled = models.BooleanField(default=True) search_default_filter = models.CharField(null=True, blank=True, max_length=128) + # User JavaScript File + customscript_enabled = models.BooleanField(default=False) + + # This is a user-defined file that will be injected at serve time by our + # Cloudflare Worker if defined + customscript_src = models.CharField( + max_length=512, + null=True, + blank=True, + help_text="URL to a JavaScript file to inject at serve time", + ) + # Notifications notifications_enabled = models.BooleanField(default=True) notifications_show_on_latest = models.BooleanField(default=True) diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index 9739d2e614c..f6a293be783 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -164,6 +164,10 @@ "default_filter": "project:project/latest", "filters": [] }, + "customscript": { + "enabled": false, + "src": null + }, "linkpreviews": { "enabled": false, "root_selector": "[role=main] a.internal", diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 51bf9d60fbc..56cb89781a1 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -490,6 +490,10 @@ def _v1(self, project, version, build, filename, url, request): # "filepath": "/docs/index.rst", # }, }, + "customscript": { + "enabled": project.addons.customscript_enabled, + "src": project.addons.customscript_src, + }, "search": { "enabled": project.addons.search_enabled, # TODO: figure it out where this data comes from. From 4d9df3fdf82bf4e8fc1943e7b9269da2397df007 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Nov 2024 17:29:36 +0100 Subject: [PATCH 4/6] Fix migrations dependency --- .../migrations/0134_addons_load_when_embedded_notnull.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py index 6481657f100..b08aeca53e4 100644 --- a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py +++ b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): safe = Safe.after_deploy dependencies = [ - ('projects', '0133_addons_options_load_when_embedded'), + ('projects', '0133_addons_load_when_embedded'), ] operations = [ From 0319397e64cea6b4b40ee5031ca162f5dc7973e0 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Nov 2024 17:35:06 +0100 Subject: [PATCH 5/6] Rename in tests --- readthedocs/proxito/tests/responses/v1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index f6a293be783..9a9920754e6 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -121,7 +121,7 @@ } }, "addons": { - "configs": { + "options": { "load_when_embedded": false }, "analytics": { From 1dad86b6a33875f7534434a1bd83a02f0c31774f Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Nov 2024 17:41:10 +0100 Subject: [PATCH 6/6] Typo --- readthedocs/proxito/views/hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 56cb89781a1..07ad75c7d94 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -455,7 +455,7 @@ def _v1(self, project, version, build, filename, url, request): # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "addons": { "options": { - "load_when_embedded": project.addons.load_when_embedded, + "load_when_embedded": project.addons.options_load_when_embedded, }, "analytics": { "enabled": project.addons.analytics_enabled,