From 0d4f76b7f78ce1c5ef6ae3e5c8627ee95a0958fb Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Wed, 11 May 2022 12:27:24 +0200
Subject: [PATCH] start the maintenance application (#64868)

---
 hobo/maintenance/forms.py                     | 26 +++++++
 .../templates/hobo/maintenance/home.html      | 31 +++++++++
 hobo/maintenance/urls.py                      | 23 +++++++
 hobo/maintenance/views.py                     | 69 +++++++++++++++++++
 hobo/settings.py                              |  1 +
 hobo/templates/hobo/home.html                 |  1 +
 hobo/urls.py                                  |  2 +
 tests/test_maintenance.py                     | 34 +++++++++
 8 files changed, 187 insertions(+)
 create mode 100644 hobo/maintenance/forms.py
 create mode 100644 hobo/maintenance/templates/hobo/maintenance/home.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..1352a76
--- /dev/null
+++ b/hobo/maintenance/forms.py
@@ -0,0 +1,26 @@
+# 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_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/templates/hobo/maintenance/home.html b/hobo/maintenance/templates/hobo/maintenance/home.html
new file mode 100644
index 0000000..bf376f7
--- /dev/null
+++ b/hobo/maintenance/templates/hobo/maintenance/home.html
@@ -0,0 +1,31 @@
+{% 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 %}
+{% if pass_through_ips %}
+<div class="infonotice">
+  {% blocktrans %}IP addresses configured to pass through the maintenance page:{% endblocktrans %} {{ pass_through_ips }}
+</div>
+{% else %}
+<div class="warningnotice">
+  {% trans "No IP addresses configured to pass through the maintenance page." %}
+</div>
+{% endif %}
+
+<form action="." method="post">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <div class="buttons">
+    <button>{% trans "Submit" %}</button>
+  </div>
+</form>
+{% endblock %}
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 <http://www.gnu.org/licenses/>.
+
+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..8ff5117
--- /dev/null
+++ b/hobo/maintenance/views.py
@@ -0,0 +1,69 @@
+# 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_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_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_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/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..4769ba0 100644
--- a/hobo/templates/hobo/home.html
+++ b/hobo/templates/hobo/home.html
@@ -117,6 +117,7 @@ $(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>
+ <a class="button button-paragraph" href="{% url 'maintenance-home' %}">{% trans 'Maintenance' %}</a>
 
 </aside>
 {% 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/tests/test_maintenance.py b/tests/test_maintenance.py
index 7ab4b07..04aac20 100644
--- a/tests/test_maintenance.py
+++ b/tests/test_maintenance.py
@@ -1,6 +1,8 @@
 import mock
 from test_manager import login
 
+from hobo.environment.models import Variable
+
 
 def test_maintenance_middleware(app, admin_user, db, monkeypatch, settings):
     app = login(app)
@@ -21,3 +23,35 @@ 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_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('/maintenance/')
+    assert 'No IP addresses configured' in resp.text
+    resp.form.set('maintenance_page', True)
+    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_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_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_PASS_THROUGH_HEADER').get().value == ''
+    assert Variable.objects.filter(name='SETTING_TENANT_DISABLE_CRON_JOBS').get().value == 'false'
+
+    settings.MAINTENANCE_PASS_THROUGH_IPS = ['127.0.0.1', '127.0.0.2']
+    resp = app.get('/maintenance/')
+    assert 'IP addresses configured to pass through the maintenance page: 127.0.0.1, 127.0.0.2' in resp.text
-- 
2.30.2

