From 7c4d274df954853cac49e30b9cb167749ad39e43 Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Wed, 11 May 2022 12:27:24 +0200 Subject: [PATCH] start the maintenance application (#64868) --- hobo/maintenance/forms.py | 29 +++++++ .../commands/disable_maintenance_page.py | 17 ++++ .../templates/hobo/maintenance/home.html | 21 +++++ .../hobo/maintenance/maintenance_page.html | 8 ++ hobo/maintenance/urls.py | 23 ++++++ hobo/maintenance/views.py | 77 +++++++++++++++++++ hobo/middleware/maintenance.py | 9 ++- hobo/settings.py | 1 + hobo/templates/hobo/home.html | 2 +- hobo/urls.py | 2 + hobo/views.py | 1 + tests/test_maintenance.py | 64 ++++++++++++++- 12 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 hobo/maintenance/forms.py create mode 100644 hobo/maintenance/management/commands/disable_maintenance_page.py create mode 100644 hobo/maintenance/templates/hobo/maintenance/home.html create mode 100644 hobo/maintenance/templates/hobo/maintenance/maintenance_page.html create mode 100644 hobo/maintenance/urls.py create mode 100644 hobo/maintenance/views.py diff --git a/hobo/maintenance/forms.py b/hobo/maintenance/forms.py new file mode 100644 index 0000000..b265e46 --- /dev/null +++ b/hobo/maintenance/forms.py @@ -0,0 +1,29 @@ +# hobo - portal to configure and deploy applications +# Copyright (C) 2015-2022 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +class MaintenanceForm(forms.Form): + maintenance_page = forms.BooleanField(required=False, label=_('Enable maintenance page')) + maintenance_page_message = forms.CharField( + required=False, widget=forms.Textarea, label=_('Maintenance page message') + ) + maintenance_pass_trough_header = forms.CharField( + required=False, label=_('Maintenance HTTP header pass through') + ) + disable_cron = forms.BooleanField(required=False, label=_('Disable cron jobs')) diff --git a/hobo/maintenance/management/commands/disable_maintenance_page.py b/hobo/maintenance/management/commands/disable_maintenance_page.py new file mode 100644 index 0000000..38275bf --- /dev/null +++ b/hobo/maintenance/management/commands/disable_maintenance_page.py @@ -0,0 +1,17 @@ +from django.core.management.base import BaseCommand, CommandError + +from hobo.environment.models import Variable +from hobo.environment.utils import get_setting_variable + + +class Command(BaseCommand): + help = 'Toggle maintenance page' + + def handle(self, *args, **options): + maintenance_page_variable = get_setting_variable('MAINTENANCE_PAGE') + if not bool(maintenance_page_variable.json): + self.stdout.write(self.style.SUCCESS('The maintenance page is already disabled.')) + return + maintenance_page_variable.json = False + maintenance_page_variable.save() + self.stdout.write(self.style.SUCCESS('Maintenance page disabled.')) diff --git a/hobo/maintenance/templates/hobo/maintenance/home.html b/hobo/maintenance/templates/hobo/maintenance/home.html new file mode 100644 index 0000000..cac78b6 --- /dev/null +++ b/hobo/maintenance/templates/hobo/maintenance/home.html @@ -0,0 +1,21 @@ +{% extends "hobo/base.html" %} +{% load i18n %} + +{% block breadcrumb %} +{{ block.super }} +{% trans "Maintenance" %} +{% endblock %} + +{% block appbar %} +

{% trans 'Maintenance' %}

+{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+{% endblock %} diff --git a/hobo/maintenance/templates/hobo/maintenance/maintenance_page.html b/hobo/maintenance/templates/hobo/maintenance/maintenance_page.html new file mode 100644 index 0000000..d5289aa --- /dev/null +++ b/hobo/maintenance/templates/hobo/maintenance/maintenance_page.html @@ -0,0 +1,8 @@ +{% load i18n %} + + + +

{% trans "This site is currently unavailable." %}

+

{{ maintenance_message|default:"" }}

+ + diff --git a/hobo/maintenance/urls.py b/hobo/maintenance/urls.py new file mode 100644 index 0000000..0706594 --- /dev/null +++ b/hobo/maintenance/urls.py @@ -0,0 +1,23 @@ +# hobo - portal to configure and deploy applications +# Copyright (C) 2015-2022 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.home, name='maintenance-home'), +] diff --git a/hobo/maintenance/views.py b/hobo/maintenance/views.py new file mode 100644 index 0000000..4d4ab5c --- /dev/null +++ b/hobo/maintenance/views.py @@ -0,0 +1,77 @@ +# hobo - portal to configure and deploy applications +# Copyright (C) 2015-2022 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings +from django.urls import reverse_lazy +from django.utils.functional import cached_property +from django.views.generic import FormView + +from hobo.environment.utils import get_setting_variable + +from .forms import MaintenanceForm + + +class HomeView(FormView): + template_name = 'hobo/maintenance/home.html' + form_class = MaintenanceForm + success_url = reverse_lazy('maintenance-home') + + @cached_property + def maintenance_page_variable(self): + return get_setting_variable('MAINTENANCE_PAGE') + + @cached_property + def maintenance_page_message_variable(self): + return get_setting_variable('MAINTENANCE_PAGE_MESSAGE') + + @cached_property + def maintenance_pass_trough_header_variable(self): + return get_setting_variable('MAINTENANCE_PASS_THROUGH_HEADER') + + @cached_property + def tenant_disable_cron_jobs_variable(self): + return get_setting_variable('TENANT_DISABLE_CRON_JOBS') + + def get_initial(self): + initial = super().get_initial() + initial['maintenance_page'] = bool(self.maintenance_page_variable.json) + initial['maintenance_page_message'] = self.maintenance_page_message_variable.value + initial['maintenance_pass_trough_header'] = self.maintenance_pass_trough_header_variable.value + initial['disable_cron'] = bool(self.tenant_disable_cron_jobs_variable.json) + return initial + + def form_valid(self, form): + self.maintenance_page_variable.json = form.cleaned_data['maintenance_page'] + self.maintenance_page_variable.save() + self.maintenance_page_message_variable.value = form.cleaned_data['maintenance_page_message'] + self.maintenance_page_message_variable.save() + + self.maintenance_pass_trough_header_variable.value = form.cleaned_data[ + 'maintenance_pass_trough_header' + ] + self.maintenance_pass_trough_header_variable.save() + self.tenant_disable_cron_jobs_variable.json = form.cleaned_data['disable_cron'] + self.tenant_disable_cron_jobs_variable.save() + return super().form_valid(form) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + pass_through_ips_setting = getattr(settings, 'MAINTENANCE_PASS_THROUGH_IPS', []) + ctx['pass_through_ips'] = ', '.join(pass_through_ips_setting) + return ctx + + +home = HomeView.as_view() diff --git a/hobo/middleware/maintenance.py b/hobo/middleware/maintenance.py index 192a153..d8a7025 100644 --- a/hobo/middleware/maintenance.py +++ b/hobo/middleware/maintenance.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from django.conf import settings -from django.http import HttpResponse +from django.template.response import TemplateResponse from django.utils.translation import ugettext as _ @@ -38,6 +38,9 @@ class MaintenanceMiddleware: def __call__(self, request): maintenance_mode = getattr(settings, 'MAINTENANCE_PAGE', None) if maintenance_mode and not pass_through(request): - maintenance_msg = _('The site is under maintenance') - return HttpResponse('

%s

' % maintenance_msg, status=503) + maintenance_message = getattr(settings, 'MAINTENANCE_PAGE_MESSAGE', '') + context = {'maintenance_message': maintenance_message} + return TemplateResponse( + request, 'hobo/maintenance/maintenance_page.html', context=context, status=503 + ).render() return self.get_response(request) diff --git a/hobo/settings.py b/hobo/settings.py index 4939c29..94bb586 100644 --- a/hobo/settings.py +++ b/hobo/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = ( 'hobo.debug', 'hobo.environment', 'hobo.franceconnect', + 'hobo.maintenance', 'hobo.matomo', 'hobo.profile', 'hobo.seo', diff --git a/hobo/templates/hobo/home.html b/hobo/templates/hobo/home.html index b4f0e82..95f5831 100644 --- a/hobo/templates/hobo/home.html +++ b/hobo/templates/hobo/home.html @@ -117,6 +117,6 @@ $(function() { {% trans 'Services' %} {% trans 'Variables' %} {% trans 'Debugging' %} - + {% if show_maintenance_menu %}{% trans 'Maintenance' %}{% endif %} {% endblock %} diff --git a/hobo/urls.py b/hobo/urls.py index daa1517..a4624f5 100644 --- a/hobo/urls.py +++ b/hobo/urls.py @@ -23,6 +23,7 @@ from .debug.urls import urlpatterns as debug_urls from .emails.urls import urlpatterns as emails_urls from .environment.urls import urlpatterns as environment_urls from .franceconnect.urls import urlpatterns as franceconnect_urls +from .maintenance.urls import urlpatterns as maintenance_urls from .matomo.urls import urlpatterns as matomo_urls from .profile.urls import urlpatterns as profile_urls from .seo.urls import urlpatterns as seo_urls @@ -45,6 +46,7 @@ urlpatterns = [ url(r'^sms/', decorated_includes(admin_required, include(sms_urls))), url(r'^debug/', decorated_includes(admin_required, include(debug_urls))), url(r'^applications/', decorated_includes(admin_required, include(applications_urls))), + url(r'^maintenance/', decorated_includes(admin_required, include(maintenance_urls))), url(r'^api/health/$', health_json, name='health-json'), url(r'^menu.json$', menu_json, name='menu_json'), url(r'^hobos.json$', hobo), diff --git a/hobo/views.py b/hobo/views.py index 0e5e3b8..07f8258 100644 --- a/hobo/views.py +++ b/hobo/views.py @@ -39,6 +39,7 @@ class Home(TemplateView): context['has_authentic'] = bool(Authentic.objects.filter(secondary=False)) context['has_global_title'] = Variable.objects.filter(name='global_title').exists() context['has_default_from_email'] = Variable.objects.filter(name='default_from_email').exists() + context['show_maintenance_menu'] = bool(getattr(settings, 'MAINTENANCE_PASS_THROUGH_IPS', [])) return context diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 7ab4b07..a553c3c 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -1,6 +1,10 @@ import mock from test_manager import login +from hobo.environment.models import Variable +from hobo.environment.utils import get_setting_variable +from hobo.maintenance.management.commands.disable_maintenance_page import Command + def test_maintenance_middleware(app, admin_user, db, monkeypatch, settings): app = login(app) @@ -9,7 +13,12 @@ def test_maintenance_middleware(app, admin_user, db, monkeypatch, settings): settings.MAINTENANCE_PAGE = True resp = app.get('/', status=503) - assert 'The site is under maintenance' in resp.text + assert 'This site is currently unavailable.' in resp.text + + # check custom maintenance message + settings.MAINTENANCE_PAGE_MESSAGE = 'foobar' + resp = app.get('/', status=503) + assert 'foobar' in resp.text settings.MAINTENANCE_PASS_THROUGH_IPS = ['127.0.0.1'] resp = app.get('/') @@ -21,3 +30,56 @@ def test_maintenance_middleware(app, admin_user, db, monkeypatch, settings): settings.MAINTENANCE_PASS_THROUGH_HEADER = 'X-Entrouvert' resp = app.get('/', headers={'X-Entrouvert': 'yes'}) assert resp.status_code == 200 + + +def test_manage(app, admin_user, settings): + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PAGE').count() == 0 + assert Variable.objects.filter(name='SETTING_MAINTENANCE_MESSAGE').count() == 0 + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PASS_THROUGH_HEADER').count() == 0 + assert Variable.objects.filter(name='TENANT_DISABLE_CRON_JOBS').count() == 0 + assert not getattr(settings, 'MAINTENANCE_PASS_THROUGH_IPS', []) + + login(app) + resp = app.get('/') + assert 'Maintenance' not in resp.text + settings.MAINTENANCE_PASS_THROUGH_IPS = ['127.0.0.1'] + resp = app.get('/') + assert 'Maintenance' in resp.text + + resp = app.get('/maintenance/') + resp.form.set('maintenance_page', True) + resp.form.set('maintenance_page_message', 'Foo') + resp.form.set('maintenance_pass_trough_header', 'X-Entrouvert') + resp.form.set('disable_cron', True) + resp = resp.form.submit().follow() + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PAGE').get().value == 'true' + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PAGE_MESSAGE').get().value == 'Foo' + assert ( + Variable.objects.filter(name='SETTING_MAINTENANCE_PASS_THROUGH_HEADER').get().value == 'X-Entrouvert' + ) + assert Variable.objects.filter(name='SETTING_TENANT_DISABLE_CRON_JOBS').get().value == 'true' + + resp.form.set('maintenance_page', False) + resp.form.set('maintenance_page_message', '') + resp.form.set('maintenance_pass_trough_header', '') + resp.form.set('disable_cron', False) + resp = resp.form.submit().follow() + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PAGE').get().value == 'false' + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PAGE_MESSAGE').get().value == '' + assert Variable.objects.filter(name='SETTING_MAINTENANCE_PASS_THROUGH_HEADER').get().value == '' + assert Variable.objects.filter(name='SETTING_TENANT_DISABLE_CRON_JOBS').get().value == 'false' + + +def test_disable_maintenance_page_command(db): + maintenance_page_variable = get_setting_variable('MAINTENANCE_PAGE') + assert not bool(maintenance_page_variable.json) + command = Command() + command.handle() + maintenance_page_variable = get_setting_variable('MAINTENANCE_PAGE') + assert not bool(maintenance_page_variable.json) + maintenance_page_variable.json = True + maintenance_page_variable.save() + assert bool(maintenance_page_variable.json) + command.handle() + maintenance_page_variable = get_setting_variable('MAINTENANCE_PAGE') + assert not bool(maintenance_page_variable.json)