From c8b895b93f5fab2c80e93488ba21a7c3ff0ba2a4 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 13 Sep 2023 16:29:09 +0200 Subject: [PATCH 1/6] Addons: allow users to opt-in into the beta addons I created the URL, models, view and form to prepare ourselves for the beta addons. Currently, if the `Project.addons` fields is not null we add a HTTP header to tell Cloudflare to enforce the new beta addons on this project. In the future, we can use each of the `ProjectAddonsConfig` field to let the user decide which of the addons they want to enable/disable and change their configurations. * Related: https://github.com/readthedocs/ext-theme/issues/212 --- readthedocs/projects/forms.py | 30 +++++ .../migrations/0106_add_addons_config.py | 120 ++++++++++++++++++ readthedocs/projects/models.py | 66 ++++++++++ readthedocs/projects/urls/private.py | 11 ++ readthedocs/projects/views/private.py | 10 ++ readthedocs/proxito/middleware.py | 19 ++- readthedocs/proxito/views/hosting.py | 1 + 7 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 readthedocs/projects/migrations/0106_add_addons_config.py diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index d61a29d6bd4..6516bcdb910 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -27,6 +27,7 @@ EnvironmentVariable, Feature, Project, + ProjectAddonsConfig, ProjectRelationship, WebHook, ) @@ -493,6 +494,35 @@ def clean_alias(self): return alias +class ProjectAddonsForm(forms.ModelForm): + + """Form to opt-in into new beta addons.""" + + enabled = forms.BooleanField( + label="Enable Read the Docs beta addons", + help_text="Opt-in into new beta addons", + required=False, + ) + + class Meta: + model = Project + fields = () + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + enabled = bool(self.instance.addons) + self.fields["enabled"].initial = enabled + + def save(self, commit=True): + instance = super().save(commit) + if self.cleaned_data.get("enabled", False): + self.instance.addons = ProjectAddonsConfig.objects.create() + self.instance.save() + elif self.instance.addons: + self.instance.addons.delete() + return instance + + class UserForm(forms.Form): """Project owners form.""" diff --git a/readthedocs/projects/migrations/0106_add_addons_config.py b/readthedocs/projects/migrations/0106_add_addons_config.py new file mode 100644 index 00000000000..6848ea8e4ba --- /dev/null +++ b/readthedocs/projects/migrations/0106_add_addons_config.py @@ -0,0 +1,120 @@ +# Generated by Django 4.2.5 on 2023-09-13 14:21 + +import django.db.models.deletion +import django_extensions.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0105_remove_project_urlconf"), + ] + + operations = [ + migrations.CreateModel( + name="ProjectAddonsConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ("analytics_enabled", models.BooleanField(default=True)), + ("doc_diff_enabled", models.BooleanField(default=True)), + ("doc_diff_show_additions", models.BooleanField(default=True)), + ("doc_diff_show_deletions", models.BooleanField(default=True)), + ("doc_diff_root_selector", models.CharField(blank=True, null=True)), + ("external_version_warning", models.BooleanField(default=True)), + ("ethicalads_enabled", models.BooleanField(default=True)), + ("flyout_enabled", models.BooleanField(default=True)), + ("hotkeys_enabled", models.BooleanField(default=True)), + ("search_enabled", models.BooleanField(default=True)), + ("search_default_filter", models.CharField(blank=True, null=True)), + ("stable_latest_version_warning", models.BooleanField(default=True)), + ], + options={ + "get_latest_by": "modified", + "abstract": False, + }, + ), + migrations.CreateModel( + name="AddonSearchFilter", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=128)), + ("syntaxt", models.CharField(max_length=128)), + ( + "addons", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.projectaddonsconfig", + ), + ), + ], + options={ + "get_latest_by": "modified", + "abstract": False, + }, + ), + migrations.AddField( + model_name="historicalproject", + name="addons", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="projects.projectaddonsconfig", + verbose_name="Addons project configuration", + ), + ), + migrations.AddField( + model_name="project", + name="addons", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="project", + to="projects.projectaddonsconfig", + verbose_name="Addons project configuration", + ), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 33fa84b85d7..bed78dd0cbe 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -123,6 +123,60 @@ def subproject_prefix(self): return unsafe_join_url_path(prefix, self.alias, "/") +class ProjectAddonsConfig(TimeStampedModel): + + """ + Addons project configuration. + + Store all the configuration for each of the addons. + Everything is enabled by default. + """ + + DOC_DIFF_DEFAULT_ROOT_SELECTOR = "[role=main]" + + # Analytics + analytics_enabled = models.BooleanField(default=True) + + # Docdiff + doc_diff_enabled = models.BooleanField(default=True) + doc_diff_root_selector = models.CharField(null=True, blank=True) + doc_diff_show_additions = models.BooleanField(default=True) + doc_diff_show_deletions = models.BooleanField(default=True) + doc_diff_root_selector = models.CharField(null=True, blank=True) + + # External version warning + external_version_warning = models.BooleanField(default=True) + + # EthicalAds + ethicalads_enabled = models.BooleanField(default=True) + + # Flyout + flyout_enabled = models.BooleanField(default=True) + + # Hotkeys + hotkeys_enabled = models.BooleanField(default=True) + + # Search + search_enabled = models.BooleanField(default=True) + search_default_filter = models.CharField(null=True, blank=True) + + # Stable/Latest version warning + stable_latest_version_warning = models.BooleanField(default=True) + + +class AddonSearchFilter(TimeStampedModel): + + """ + Addon search user defined filter. + + Specific filter defined by the user to show on the search modal. + """ + + addons = models.ForeignKey("ProjectAddonsConfig", on_delete=models.CASCADE) + name = models.CharField(max_length=128) + syntaxt = models.CharField(max_length=128) + + class Project(models.Model): """Project model.""" @@ -131,6 +185,18 @@ class Project(models.Model): pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True, db_index=True) modified_date = models.DateTimeField(_('Modified date'), auto_now=True, db_index=True) + # NOTE: we are defaulting this field to ``NULL``, meaning the "new beta addons" are disabled. + # When the user enables the beta addons, we create a ``AddonsProjectConfig`` and attach it + # to this ``Project`` to override the "old integration" at serve time. + addons = models.ForeignKey( + "ProjectAddonsConfig", + verbose_name=_("Addons project configuration"), + related_name="project", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + # Generally from conf.py users = models.ManyToManyField( User, diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py index e6db8a2911e..cea4855ac7b 100644 --- a/readthedocs/projects/urls/private.py +++ b/readthedocs/projects/urls/private.py @@ -26,6 +26,7 @@ IntegrationExchangeDetail, IntegrationList, IntegrationWebhookSync, + ProjectAddonsUpdate, ProjectAdvancedUpdate, ProjectAdvertisingUpdate, ProjectDashboard, @@ -204,6 +205,16 @@ urlpatterns += domain_urls +addons_urls = [ + re_path( + r"^(?P[-\w]+)/addons/edit/$$", + ProjectAddonsUpdate.as_view(), + name="projects_addons", + ), +] + +urlpatterns += addons_urls + integration_urls = [ re_path( r"^(?P{project_slug})/integrations/$".format(**pattern_opts), diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index ea925f6d484..13a65af62b5 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -50,6 +50,7 @@ EmailHookForm, EnvironmentVariableForm, IntegrationForm, + ProjectAddonsForm, ProjectAdvancedForm, ProjectAdvertisingForm, ProjectBasicsForm, @@ -168,6 +169,15 @@ def get_success_url(self): return reverse('projects_detail', args=[self.object.slug]) +class ProjectAddonsUpdate(ProjectMixin, UpdateView): + form_class = ProjectAddonsForm + success_message = _("Project addons updated") + template_name = "projects/addons_form.html" + + def get_success_url(self): + return reverse("projects_addons", args=[self.object.slug]) + + class ProjectDelete(UpdateChangeReasonPostView, ProjectMixin, DeleteView): success_message = _('Project deleted') diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index d779972124a..3bc2c5255b6 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -25,6 +25,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag +from readthedocs.projects.models import Project from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response from readthedocs.proxito.redirects import redirect_to_https @@ -269,12 +270,22 @@ def add_hosting_integrations_headers(self, request, response): project_slug = getattr(request, "path_project_slug", "") version_slug = getattr(request, "path_version_slug", "") - if project_slug and version_slug: - addons = Version.objects.filter( + if project_slug: + force_addons = Project.objects.filter( project__slug=project_slug, - slug=version_slug, - addons=True, + addons__isnull=False, ).exists() + if force_addons: + response["X-RTD-Force-Addons"] = "true" + return + + if version_slug: + addons = Version.objects.filter( + project__slug=project_slug, + slug=version_slug, + addons=True, + ).exists() + if addons: response["X-RTD-Hosting-Integrations"] = "true" diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index f957596e0d1..29f42c62dd4 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -239,6 +239,7 @@ def _v0(self, project, version, build, filename, user): project.translations.all().only("language").order_by("language") ) # Make one DB query here and then check on Python code + # TODO: make usage of ``Project.addons._enabled`` to decide if enabled project_features = project.features.all().values_list("feature_id", flat=True) data = { From 509a17f71d1bcf4c8dcadb5ba1195cd0819e25ab Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 14 Sep 2023 11:40:25 +0200 Subject: [PATCH 2/6] Changes from feedback Use a `AddonsConfig.enabled` field to decide whether or not all the addons are enabled. --- readthedocs/projects/forms.py | 33 +++++------ .../migrations/0106_add_addons_config.py | 55 +++++++++---------- readthedocs/projects/models.py | 34 ++++++------ readthedocs/projects/urls/private.py | 4 +- readthedocs/projects/views/private.py | 8 +-- 5 files changed, 62 insertions(+), 72 deletions(-) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 6516bcdb910..667fd1b278e 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -22,12 +22,12 @@ from readthedocs.invitations.models import Invitation from readthedocs.oauth.models import RemoteRepository from readthedocs.projects.models import ( + AddonsConfig, Domain, EmailHook, EnvironmentVariable, Feature, Project, - ProjectAddonsConfig, ProjectRelationship, WebHook, ) @@ -494,33 +494,28 @@ def clean_alias(self): return alias -class ProjectAddonsForm(forms.ModelForm): +class AddonsConfigForm(forms.ModelForm): """Form to opt-in into new beta addons.""" - enabled = forms.BooleanField( - label="Enable Read the Docs beta addons", - help_text="Opt-in into new beta addons", - required=False, - ) + project = forms.CharField(widget=forms.HiddenInput(), required=False) class Meta: - model = Project - fields = () + model = AddonsConfig + fields = ("enabled", "project") def __init__(self, *args, **kwargs): + self.project = kwargs.pop("project", None) + kwargs["instance"] = getattr(self.project, "addons", None) super().__init__(*args, **kwargs) - enabled = bool(self.instance.addons) - self.fields["enabled"].initial = enabled - def save(self, commit=True): - instance = super().save(commit) - if self.cleaned_data.get("enabled", False): - self.instance.addons = ProjectAddonsConfig.objects.create() - self.instance.save() - elif self.instance.addons: - self.instance.addons.delete() - return instance + try: + self.fields["enabled"].initial = self.project.addons.enabled + except AddonsConfig.DoesNotExist: + self.fields["enabled"].initial = False + + def clean_project(self): + return self.project class UserForm(forms.Form): diff --git a/readthedocs/projects/migrations/0106_add_addons_config.py b/readthedocs/projects/migrations/0106_add_addons_config.py index 6848ea8e4ba..d36408b4972 100644 --- a/readthedocs/projects/migrations/0106_add_addons_config.py +++ b/readthedocs/projects/migrations/0106_add_addons_config.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-09-13 14:21 +# Generated by Django 4.2.5 on 2023-09-14 09:21 import django.db.models.deletion import django_extensions.db.fields @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="ProjectAddonsConfig", + name="AddonsConfig", fields=[ ( "id", @@ -35,18 +35,38 @@ class Migration(migrations.Migration): auto_now=True, verbose_name="modified" ), ), + ( + "enabled", + models.BooleanField( + default=True, + help_text="Enable/Disable all the addons on this project", + ), + ), ("analytics_enabled", models.BooleanField(default=True)), ("doc_diff_enabled", models.BooleanField(default=True)), ("doc_diff_show_additions", models.BooleanField(default=True)), ("doc_diff_show_deletions", models.BooleanField(default=True)), ("doc_diff_root_selector", models.CharField(blank=True, null=True)), - ("external_version_warning", models.BooleanField(default=True)), + ("external_version_warning_enabled", models.BooleanField(default=True)), ("ethicalads_enabled", models.BooleanField(default=True)), ("flyout_enabled", models.BooleanField(default=True)), ("hotkeys_enabled", models.BooleanField(default=True)), ("search_enabled", models.BooleanField(default=True)), ("search_default_filter", models.CharField(blank=True, null=True)), - ("stable_latest_version_warning", models.BooleanField(default=True)), + ( + "stable_latest_version_warning_enabled", + models.BooleanField(default=True), + ), + ( + "project", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="addons", + to="projects.project", + ), + ), ], options={ "get_latest_by": "modified", @@ -83,7 +103,7 @@ class Migration(migrations.Migration): "addons", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to="projects.projectaddonsconfig", + to="projects.addonsconfig", ), ), ], @@ -92,29 +112,4 @@ class Migration(migrations.Migration): "abstract": False, }, ), - migrations.AddField( - model_name="historicalproject", - name="addons", - field=models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="projects.projectaddonsconfig", - verbose_name="Addons project configuration", - ), - ), - migrations.AddField( - model_name="project", - name="addons", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="project", - to="projects.projectaddonsconfig", - verbose_name="Addons project configuration", - ), - ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index bed78dd0cbe..94c75cfe3ab 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -123,7 +123,7 @@ def subproject_prefix(self): return unsafe_join_url_path(prefix, self.alias, "/") -class ProjectAddonsConfig(TimeStampedModel): +class AddonsConfig(TimeStampedModel): """ Addons project configuration. @@ -134,18 +134,30 @@ class ProjectAddonsConfig(TimeStampedModel): DOC_DIFF_DEFAULT_ROOT_SELECTOR = "[role=main]" + project = models.OneToOneField( + "Project", + related_name="addons", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + + enabled = models.BooleanField( + default=True, + help_text="Enable/Disable all the addons on this project", + ) + # Analytics analytics_enabled = models.BooleanField(default=True) # Docdiff doc_diff_enabled = models.BooleanField(default=True) - doc_diff_root_selector = models.CharField(null=True, blank=True) doc_diff_show_additions = models.BooleanField(default=True) doc_diff_show_deletions = models.BooleanField(default=True) doc_diff_root_selector = models.CharField(null=True, blank=True) # External version warning - external_version_warning = models.BooleanField(default=True) + external_version_warning_enabled = models.BooleanField(default=True) # EthicalAds ethicalads_enabled = models.BooleanField(default=True) @@ -161,7 +173,7 @@ class ProjectAddonsConfig(TimeStampedModel): search_default_filter = models.CharField(null=True, blank=True) # Stable/Latest version warning - stable_latest_version_warning = models.BooleanField(default=True) + stable_latest_version_warning_enabled = models.BooleanField(default=True) class AddonSearchFilter(TimeStampedModel): @@ -172,7 +184,7 @@ class AddonSearchFilter(TimeStampedModel): Specific filter defined by the user to show on the search modal. """ - addons = models.ForeignKey("ProjectAddonsConfig", on_delete=models.CASCADE) + addons = models.ForeignKey("AddonsConfig", on_delete=models.CASCADE) name = models.CharField(max_length=128) syntaxt = models.CharField(max_length=128) @@ -185,18 +197,6 @@ class Project(models.Model): pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True, db_index=True) modified_date = models.DateTimeField(_('Modified date'), auto_now=True, db_index=True) - # NOTE: we are defaulting this field to ``NULL``, meaning the "new beta addons" are disabled. - # When the user enables the beta addons, we create a ``AddonsProjectConfig`` and attach it - # to this ``Project`` to override the "old integration" at serve time. - addons = models.ForeignKey( - "ProjectAddonsConfig", - verbose_name=_("Addons project configuration"), - related_name="project", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - # Generally from conf.py users = models.ManyToManyField( User, diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py index cea4855ac7b..c378cfefcac 100644 --- a/readthedocs/projects/urls/private.py +++ b/readthedocs/projects/urls/private.py @@ -9,6 +9,7 @@ from readthedocs.projects.backends.views import ImportWizardView from readthedocs.projects.views import private from readthedocs.projects.views.private import ( + AddonsConfigUpdate, AutomationRuleDelete, AutomationRuleList, AutomationRuleMove, @@ -26,7 +27,6 @@ IntegrationExchangeDetail, IntegrationList, IntegrationWebhookSync, - ProjectAddonsUpdate, ProjectAdvancedUpdate, ProjectAdvertisingUpdate, ProjectDashboard, @@ -208,7 +208,7 @@ addons_urls = [ re_path( r"^(?P[-\w]+)/addons/edit/$$", - ProjectAddonsUpdate.as_view(), + AddonsConfigUpdate.as_view(), name="projects_addons", ), ] diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 13a65af62b5..b6387ff895d 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -46,11 +46,11 @@ from readthedocs.oauth.utils import update_webhook from readthedocs.projects.filters import ProjectListFilterSet from readthedocs.projects.forms import ( + AddonsConfigForm, DomainForm, EmailHookForm, EnvironmentVariableForm, IntegrationForm, - ProjectAddonsForm, ProjectAdvancedForm, ProjectAdvertisingForm, ProjectBasicsForm, @@ -169,13 +169,13 @@ def get_success_url(self): return reverse('projects_detail', args=[self.object.slug]) -class ProjectAddonsUpdate(ProjectMixin, UpdateView): - form_class = ProjectAddonsForm +class AddonsConfigUpdate(ProjectAdminMixin, PrivateViewMixin, CreateView, UpdateView): + form_class = AddonsConfigForm success_message = _("Project addons updated") template_name = "projects/addons_form.html" def get_success_url(self): - return reverse("projects_addons", args=[self.object.slug]) + return reverse("projects_addons", args=[self.object.project.slug]) class ProjectDelete(UpdateChangeReasonPostView, ProjectMixin, DeleteView): From 4595bd42fe28fd739edab5cde2fd08ee14a70ca2 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Sep 2023 13:51:01 +0200 Subject: [PATCH 3/6] Docstring to document the HTTP headers to communicate with CF --- readthedocs/proxito/middleware.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 3bc2c5255b6..86382e539a5 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -266,6 +266,22 @@ def process_request(self, request): # noqa return None def add_hosting_integrations_headers(self, request, response): + """ + Add HTTP headers to communicate to Cloudflare Workers. + + We have configured Cloudflare Workers to inject the addons and remove + the old flyout integration based on HTTP headers. + This method uses two different headers for these purposes: + + - ``X-RTD-Hosting-Integrations``: inject ``readthedocs-addons.js`` to enable addons. + Enabled by default on projects using ``build.commands``. + - ``X-RTD-Force-Addons``: inject ``readthedocs-addons.js`` + and remove old flyout integration (via ``readthedocs-doc-embed.js``). + Enabled only on projects that opted-in via the admin settings. + + Note these headers will not be required anymore eventually + since all the project will be using the new addons once we fully roll them out. + """ addons = False project_slug = getattr(request, "path_project_slug", "") version_slug = getattr(request, "path_version_slug", "") From 339f11dc2a53d7a66a2497f8a4bae65aad809608 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Sep 2023 14:06:18 +0200 Subject: [PATCH 4/6] Addons: update modelling and queryset --- .../projects/migrations/0106_add_addons_config.py | 12 +++++++++--- readthedocs/projects/models.py | 4 ++-- readthedocs/proxito/middleware.py | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/readthedocs/projects/migrations/0106_add_addons_config.py b/readthedocs/projects/migrations/0106_add_addons_config.py index d36408b4972..cc7319daead 100644 --- a/readthedocs/projects/migrations/0106_add_addons_config.py +++ b/readthedocs/projects/migrations/0106_add_addons_config.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-09-14 09:21 +# Generated by Django 4.2.5 on 2023-09-18 11:57 import django.db.models.deletion import django_extensions.db.fields @@ -46,13 +46,19 @@ class Migration(migrations.Migration): ("doc_diff_enabled", models.BooleanField(default=True)), ("doc_diff_show_additions", models.BooleanField(default=True)), ("doc_diff_show_deletions", models.BooleanField(default=True)), - ("doc_diff_root_selector", models.CharField(blank=True, null=True)), + ( + "doc_diff_root_selector", + models.CharField(blank=True, max_length=128, null=True), + ), ("external_version_warning_enabled", models.BooleanField(default=True)), ("ethicalads_enabled", models.BooleanField(default=True)), ("flyout_enabled", models.BooleanField(default=True)), ("hotkeys_enabled", models.BooleanField(default=True)), ("search_enabled", models.BooleanField(default=True)), - ("search_default_filter", models.CharField(blank=True, null=True)), + ( + "search_default_filter", + models.CharField(blank=True, max_length=128, null=True), + ), ( "stable_latest_version_warning_enabled", models.BooleanField(default=True), diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 94c75cfe3ab..ac5c5d359ee 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -154,7 +154,7 @@ class AddonsConfig(TimeStampedModel): doc_diff_enabled = models.BooleanField(default=True) doc_diff_show_additions = models.BooleanField(default=True) doc_diff_show_deletions = models.BooleanField(default=True) - doc_diff_root_selector = models.CharField(null=True, blank=True) + doc_diff_root_selector = models.CharField(null=True, blank=True, max_length=128) # External version warning external_version_warning_enabled = models.BooleanField(default=True) @@ -170,7 +170,7 @@ class AddonsConfig(TimeStampedModel): # Search search_enabled = models.BooleanField(default=True) - search_default_filter = models.CharField(null=True, blank=True) + search_default_filter = models.CharField(null=True, blank=True, max_length=128) # Stable/Latest version warning stable_latest_version_warning_enabled = models.BooleanField(default=True) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 86382e539a5..0d06839b085 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -288,8 +288,8 @@ def add_hosting_integrations_headers(self, request, response): if project_slug: force_addons = Project.objects.filter( - project__slug=project_slug, - addons__isnull=False, + slug=project_slug, + addons__enabled=True, ).exists() if force_addons: response["X-RTD-Force-Addons"] = "true" From 2c493fce76aa0a6ad917fc3d25338233a2949644 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Sep 2023 14:07:06 +0200 Subject: [PATCH 5/6] NGINX: pass the `X-RTD-Force-Addons` HTTP header --- dockerfiles/nginx/proxito.conf.template | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dockerfiles/nginx/proxito.conf.template b/dockerfiles/nginx/proxito.conf.template index 44559b7d4da..fdff76e9984 100644 --- a/dockerfiles/nginx/proxito.conf.template +++ b/dockerfiles/nginx/proxito.conf.template @@ -93,6 +93,8 @@ server { # Now, I'm injecting it in all the NGINX responses because `sub_filter` is not allowed inside an `if` statement. set $rtd_hosting_integrations $upstream_http_x_rtd_hosting_integrations; add_header X-RTD-Hosting-Integrations $rtd_hosting_integrations always; + set $rtd_force_addons $upstream_http_x_rtd_force_addons; + add_header X-RTD-Force-Addons $rtd_force_addons always; # Inject our own script dynamically # TODO: find a way to make this work _without_ running `npm run dev` from the `addons` repository From c666621f7f146d6122dc2b3e0b19d588ebb6d628 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Sep 2023 14:35:04 +0200 Subject: [PATCH 6/6] Declare the URL only when ext-theme is enabled --- readthedocs/projects/urls/private.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py index c378cfefcac..8a926f8c5d6 100644 --- a/readthedocs/projects/urls/private.py +++ b/readthedocs/projects/urls/private.py @@ -205,15 +205,17 @@ urlpatterns += domain_urls -addons_urls = [ - re_path( - r"^(?P[-\w]+)/addons/edit/$$", - AddonsConfigUpdate.as_view(), - name="projects_addons", - ), -] +# We are allowing users to enable the new beta addons only from the new dashboard +if settings.RTD_EXT_THEME_ENABLED: + addons_urls = [ + re_path( + r"^(?P[-\w]+)/addons/edit/$$", + AddonsConfigUpdate.as_view(), + name="projects_addons", + ), + ] -urlpatterns += addons_urls + urlpatterns += addons_urls integration_urls = [ re_path(