start the maintenance application (#64868)

This commit is contained in:
Emmanuel Cazenave 2022-05-11 12:27:24 +02:00
parent 1700420a1e
commit 7c4d274df9
12 changed files with 249 additions and 5 deletions

29
hobo/maintenance/forms.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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'))

View File

@ -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.'))

View File

@ -0,0 +1,21 @@
{% extends "hobo/base.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'maintenance-home' %}">{% trans "Maintenance" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Maintenance' %}</h2>
{% endblock %}
{% block content %}
<form action="." method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button>{% trans "Submit" %}</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% load i18n %}
<html>
<body>
<h1>{% trans "This site is currently unavailable." %}</h1>
<p>{{ maintenance_message|default:"" }}</p>
</body>
</html>

23
hobo/maintenance/urls.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.home, name='maintenance-home'),
]

77
hobo/maintenance/views.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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()

View File

@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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('<h1>%s</h1>' % 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)

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = (
'hobo.debug',
'hobo.environment',
'hobo.franceconnect',
'hobo.maintenance',
'hobo.matomo',
'hobo.profile',
'hobo.seo',

View File

@ -117,6 +117,6 @@ $(function() {
<a class="button button-paragraph" href="{% url 'environment-home' %}">{% trans 'Services' %}</a>
<a class="button button-paragraph" href="{% url 'environment-variables' %}">{% trans 'Variables' %}</a>
<a class="button button-paragraph" href="{% url 'debug-home' %}">{% trans 'Debugging' %}</a>
{% if show_maintenance_menu %}<a class="button button-paragraph" href="{% url 'maintenance-home' %}">{% trans 'Maintenance' %}</a>{% endif %}
</aside>
{% endblock %}

View File

@ -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),

View File

@ -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

View File

@ -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)