From b37fdd68a9ffa22266a4c27161994b7a1adbff6b Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Tue, 22 Jan 2019 14:34:56 +0100
Subject: [PATCH 1/2] manage availability check through the UI (#29965)

---
 .../migrations/0011_auto_20190205_1126.py     | 30 +++++++++++
 passerelle/base/models.py                     | 30 +++++++++++
 passerelle/base/urls.py                       |  7 ++-
 passerelle/base/views.py                      | 54 ++++++++++++++++++-
 passerelle/contrib/planitech/models.py        |  5 ++
 .../manage/manage_availability_form.html      | 17 ++++++
 .../passerelle/manage/service_view.html       |  3 ++
 passerelle/views.py                           |  7 +++
 tests/test_availability.py                    | 20 +++++++
 tests/test_manager.py                         | 53 +++++++++++++++++-
 10 files changed, 222 insertions(+), 4 deletions(-)
 create mode 100644 passerelle/base/migrations/0011_auto_20190205_1126.py
 create mode 100644 passerelle/templates/passerelle/manage/manage_availability_form.html

diff --git a/passerelle/base/migrations/0011_auto_20190205_1126.py b/passerelle/base/migrations/0011_auto_20190205_1126.py
new file mode 100644
index 0000000..1a4b006
--- /dev/null
+++ b/passerelle/base/migrations/0011_auto_20190205_1126.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2019-02-05 10:26
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('base', '0010_loggingparameters_trace_emails'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AvailabilityParameters',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('resource_pk', models.PositiveIntegerField()),
+                ('run_check', models.BooleanField(default=True, verbose_name='Run regular availability checks')),
+                ('resource_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='availabilityparameters',
+            unique_together=set([('resource_type', 'resource_pk')]),
+        ),
+    ]
diff --git a/passerelle/base/models.py b/passerelle/base/models.py
index d692143..6904dcb 100644
--- a/passerelle/base/models.py
+++ b/passerelle/base/models.py
@@ -176,6 +176,18 @@ class BaseResource(models.Model):
         parameters.log_level = value
         parameters.save()
 
+    @property
+    def availability_parameters(self):
+        resource_type = ContentType.objects.get_for_model(self)
+        try:
+            return AvailabilityParameters.objects.get(
+                    resource_type=resource_type,
+                    resource_pk=self.id)
+        except AvailabilityParameters.DoesNotExist:
+            return AvailabilityParameters(
+                    resource_type=resource_type,
+                    resource_pk=self.id)
+
     def soap_client(self, **kwargs):
         return passerelle.utils.SOAPClient(resource=self, **kwargs)
 
@@ -387,9 +399,15 @@ class BaseResource(models.Model):
     def check_status(self):
         # should raise an exception if status is not ok
         raise NotImplementedError
+    check_status.not_implemented = True
 
     def availability(self):
         # "availability" cron job to update service statuses
+
+        # eventually skip it
+        if not self.availability_parameters.run_check:
+            return
+
         currently_down = self.down()
         try:
             self.check_status()
@@ -477,6 +495,18 @@ class LoggingParameters(models.Model):
         unique_together = (('resource_type', 'resource_pk'))
 
 
+class AvailabilityParameters(models.Model):
+    resource_type = models.ForeignKey(ContentType)
+    resource_pk = models.PositiveIntegerField()
+    resource = fields.GenericForeignKey('resource_type', 'resource_pk')
+    run_check = models.BooleanField(
+        default=True, verbose_name=_('Run regular availability checks'),
+        help_text=_('Run an availability check every 5 minutes'))
+
+    class Meta:
+        unique_together = (('resource_type', 'resource_pk'))
+
+
 
 class ResourceLog(models.Model):
     timestamp = models.DateTimeField(auto_now_add=True)
diff --git a/passerelle/base/urls.py b/passerelle/base/urls.py
index 1cdfe1d..33536a3 100644
--- a/passerelle/base/urls.py
+++ b/passerelle/base/urls.py
@@ -2,7 +2,7 @@ from django.conf.urls import url
 
 from .views import ApiUserCreateView, ApiUserUpdateView, ApiUserDeleteView, \
         ApiUserListView, AccessRightDeleteView, AccessRightCreateView, \
-        LoggingParametersUpdateView
+        LoggingParametersUpdateView, ManageAvailabilityView
 
 access_urlpatterns = [
     url(r'^$', ApiUserListView.as_view(), name='apiuser-list'),
@@ -15,5 +15,8 @@ access_urlpatterns = [
     url(r'^accessright/add/(?P<resource_type>[\w,-]+)/(?P<resource_pk>[\w,-]+)/(?P<codename>[\w,-]+)/',
         AccessRightCreateView.as_view(), name='access-right-add'),
     url(r'logging/parameters/(?P<resource_type>[\w,-]+)/(?P<resource_pk>[\w,-]+)/$',
-        LoggingParametersUpdateView.as_view(), name='logging-parameters')
+        LoggingParametersUpdateView.as_view(), name='logging-parameters'),
+    url(r'manage/availability/(?P<resource_type>[\w,-]+)/(?P<resource_pk>[\w,-]+)/$',
+        ManageAvailabilityView.as_view(), name='manage-availability')
+
 ]
diff --git a/passerelle/base/views.py b/passerelle/base/views.py
index 20b4a13..51072ca 100644
--- a/passerelle/base/views.py
+++ b/passerelle/base/views.py
@@ -5,7 +5,7 @@ from django.forms import models as model_forms
 from django.views.generic import *
 from django.http import Http404
 
-from .models import ApiUser, AccessRight, LoggingParameters
+from .models import ApiUser, AccessRight, LoggingParameters, AvailabilityParameters, ResourceStatus
 from .forms import ApiUserForm, AccessRightForm
 from ..utils import get_trusted_services
 
@@ -30,6 +30,7 @@ class ResourceView(DetailView):
         context['absolute_uri'] = '%s%s' % (
                 context['site_base_uri'],
                 self.request.path)
+
         return context
 
 
@@ -136,3 +137,54 @@ class LoggingParametersUpdateView(FormView):
         parameters.trace_emails = form.cleaned_data['trace_emails']
         parameters.save()
         return super(LoggingParametersUpdateView, self).form_valid(form)
+
+
+class ManageAvailabilityView(FormView):
+    template_name = 'passerelle/manage/manage_availability_form.html'
+
+    def get_context_data(self, **kwargs):
+        context = super(ManageAvailabilityView, self).get_context_data(**kwargs)
+        connector = self.get_resource()
+        context['connector'] = connector
+        context['availability_status'] = connector.get_availability_status()
+        return context
+
+    def get_form_class(self):
+        form_class = model_forms.modelform_factory(
+                AvailabilityParameters,
+                fields=['run_check'])
+        return form_class
+
+    def get_initial(self):
+        d = self.initial.copy()
+        d['resource_type'] = self.kwargs['resource_type']
+        d['resource_pk'] = self.kwargs['resource_pk']
+        d['run_check'] = self.get_resource().availability_parameters.run_check
+        return d
+
+    def get_resource(self):
+        content_type = ContentType.objects.get_for_id(self.kwargs['resource_type'])
+        return content_type.model_class().objects.get(pk=self.kwargs['resource_pk'])
+
+    def get_success_url(self):
+        return self.get_resource().get_absolute_url()
+
+    def form_valid(self, form):
+        resource = self.get_resource()
+        parameters = resource.availability_parameters
+        run_check = form.cleaned_data['run_check']
+
+        if not run_check and resource.down():
+            resource_type = ContentType.objects.get_for_model(resource)
+            ResourceStatus(
+                resource_type=resource_type,
+                resource_pk=self.kwargs['resource_pk'],
+                status='up',
+                message='').save()
+
+        if parameters.run_check != run_check:
+            parameters.run_check = run_check
+            parameters.save()
+            resource.logger.info(u'availability checks %s', 'enabled' if run_check else 'disabled')
+
+        return super(ManageAvailabilityView, self).form_valid(form)
diff --git a/passerelle/contrib/planitech/models.py b/passerelle/contrib/planitech/models.py
index ac2ef80..2114bfa 100644
--- a/passerelle/contrib/planitech/models.py
+++ b/passerelle/contrib/planitech/models.py
@@ -602,6 +602,11 @@ class PlanitechConnector(BaseResource):
             }
         }
 
+    def check_status(self):
+        auth_url = urlparse.urljoin(self.url, 'auth')
+        response = self.requests.get(auth_url, headers={'MH-LOGIN': self.username})
+        response.raise_for_status()
+
 
 class Pairing(models.Model):
 
diff --git a/passerelle/templates/passerelle/manage/manage_availability_form.html b/passerelle/templates/passerelle/manage/manage_availability_form.html
new file mode 100644
index 0000000..ae80507
--- /dev/null
+++ b/passerelle/templates/passerelle/manage/manage_availability_form.html
@@ -0,0 +1,17 @@
+{% extends "passerelle/manage.html" %}
+{% load i18n %}
+
+{% block appbar %}
+<h2>{% trans 'Connector Status' %} : {{availability_status.status}}</h2>
+{% endblock %}
+
+{% block content %}
+<form method="post">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <div class="buttons">
+    <button class="submit-button">{% trans 'Save' %}</button>
+    <a href="{{ connector.get_absolute_url }}" class="cancel">{% trans 'Cancel' %}</a>
+  </div>
+</form>
+{% endblock %}
diff --git a/passerelle/templates/passerelle/manage/service_view.html b/passerelle/templates/passerelle/manage/service_view.html
index b375d08..4270cb5 100644
--- a/passerelle/templates/passerelle/manage/service_view.html
+++ b/passerelle/templates/passerelle/manage/service_view.html
@@ -15,6 +15,9 @@
     {% endwith %}
 </h2>
 <span class="actions">
+{% if object|can_edit:request.user and has_check_status %}
+<a rel="popup" href="{% url 'manage-availability' resource_type=object|resource_type resource_pk=object.id %}">{% trans 'availability check' %}</a>
+{% endif %}
 {% if object|can_edit:request.user %}
 <a rel="popup" href="{% url 'logging-parameters' resource_type=object|resource_type resource_pk=object.id %}">{% trans 'logging parameters' %}</a>
 {% endif %}
diff --git a/passerelle/views.py b/passerelle/views.py
index 800d53a..899cd66 100644
--- a/passerelle/views.py
+++ b/passerelle/views.py
@@ -138,6 +138,13 @@ class GenericConnectorMixin(object):
 
 
 class GenericConnectorView(GenericConnectorMixin, DetailView):
+
+    def get_context_data(self, slug=None, **kwargs):
+        context = super(GenericConnectorView, self).get_context_data(**kwargs)
+        context['has_check_status'] = not hasattr(
+            context['object'].check_status, 'not_implemented')
+        return context
+
     def get_template_names(self):
         template_names = super(DetailView, self).get_template_names()[:]
         if self.model.manager_view_template_name:
diff --git a/tests/test_availability.py b/tests/test_availability.py
index 435c5d0..bb0bd75 100644
--- a/tests/test_availability.py
+++ b/tests/test_availability.py
@@ -73,3 +73,23 @@ def test_feed_availability(app, connector):
         connector.availability()
     assert connector.get_availability_status().down()
     assert '500' in connector.get_availability_status().message
+
+
+def test_availability_checks_disabled(app, connector):
+    with HTTMock(up_mock):
+        connector.availability()
+    assert connector.get_availability_status().up()
+
+    av = connector.availability_parameters
+    av.run_check = False
+    av.save()
+
+    with HTTMock(down_mock):
+        connector.availability()
+    assert connector.get_availability_status().up()
+
+    av.run_check = True
+    av.save()
+    with HTTMock(down_mock):
+        connector.availability()
+    assert connector.get_availability_status().down()
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 39903c7..71d93e3 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.files import File
 import pytest
 
-from passerelle.base.models import ApiUser, AccessRight, ResourceLog
+from passerelle.base.models import ApiUser, AccessRight, ResourceLog, ResourceStatus
 from passerelle.apps.csvdatasource.models import CsvDataSource, Query
 
 pytestmark = pytest.mark.django_db
@@ -197,3 +197,54 @@ def test_logging_parameters(app, admin_user):
     resp = app.get(csv.get_absolute_url())
     resp = resp.click('logging parameters')
     assert resp.form['trace_emails'].value == 'fred@localhost'
+
+
+def test_availability_parameters(app, admin_user, monkeypatch):
+    data = StringIO('1;Foo\n2;Bar\n3;Baz')
+    csv = CsvDataSource.objects.create(
+        csv_file=File(data, 't.csv'),
+        columns_keynames='id, text', slug='test', title='a title', description='a description')
+    app = login(app)
+    resp = app.get(csv.get_absolute_url())
+
+    assert csv.availability_parameters.run_check
+    # csv connector has the default check_status which does nothing
+    # so availability check is hidden
+    assert 'availability check' not in resp.text
+
+    def check_status(*args, **kwargs):
+        return True
+
+    monkeypatch.setattr(CsvDataSource, 'check_status', check_status)
+
+    resp = app.get(csv.get_absolute_url())
+    assert 'availability check' in resp.text
+
+    resp = resp.click('availability check')
+    assert 'up' in resp.text
+    resp.form['run_check'] = False
+    resp = resp.form.submit()
+    # Connector status not changed, availability parameters changed
+    assert not csv.availability_parameters.run_check
+
+    resp = app.get(csv.get_absolute_url())
+    resp = resp.click('availability check')
+    resp.form['run_check'] = True
+    resp = resp.form.submit()
+
+    # Connector down
+    resource_type = ContentType.objects.get_for_model(csv)
+    status = ResourceStatus(
+        resource_type=resource_type, resource_pk=csv.pk,
+        status='down', message='')
+    status.save()
+    assert csv.down()
+    resp = app.get(csv.get_absolute_url())
+    resp = resp.click('availability check')
+    resp.form['run_check'] = False
+    resp = resp.form.submit()
+    # Connector is put back up
+    assert not csv.availability_parameters.run_check
+    assert not csv.down()
+    status = csv.get_availability_status()
+    assert status.status == 'up'
-- 
2.20.1

