6 Commits 2d3e31289d ... ee16316349

Author SHA1 Message Date
  Guilhem Saurel ee16316349 send notifications to webhooks 6 years ago
  Élie Bouttier 1314128404 restriction allocations 6 years ago
  Élie Bouttier cd80a830b3 Le champs notes des adhésions est obsolète 6 years ago
  Élie Bouttier a995ad25a0 fix ping status on adherent UI 6 years ago
  Élie Bouttier 709099f78c ip resources states history 6 years ago
  Guilhem Saurel 2d3e31289d send notifications to webhooks 6 years ago

+ 18 - 0
adhesions/migrations/0020_auto_20190227_2058.py

@@ -0,0 +1,18 @@
+# Generated by Django 2.1.1 on 2019-02-27 19:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('adhesions', '0019_auto_20180216_0016'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='adhesion',
+            name='notes',
+            field=models.TextField(blank=True, default='', help_text='Obsolète, ne plus ajouter d’informations ici.', verbose_name='Notes (obsolète)'),
+        ),
+    ]

+ 1 - 1
adhesions/models.py

@@ -79,7 +79,7 @@ class Adhesion(models.Model):
           | models.Q(app_label='adhesions', model='corporation')
     id = models.AutoField(verbose_name='Numéro d’adhérent·e', primary_key=True, editable=True)
     created = models.DateTimeField(null=True, blank=True, auto_now_add=True)
-    notes = models.TextField(blank=True, default='')
+    notes = models.TextField(blank=True, default='', verbose_name='Notes (obsolète)', help_text='Obsolète, ne plus ajouter d’informations ici.')
     active_legacy = models.NullBooleanField(default=None, verbose_name='Adhésion en cours')
 
     user = models.OneToOneField(User, null=True, on_delete=models.PROTECT)

+ 148 - 69
services/admin.py

@@ -3,7 +3,7 @@ from django.db import models
 from django.db.models import Q
 from django.forms import ModelForm, BaseInlineFormSet
 from django.utils import timezone
-from django.urls import reverse
+from django.urls import reverse, path
 from django.utils.html import format_html
 from django.conf.urls import url
 from django.template.response import TemplateResponse
@@ -26,11 +26,11 @@ from datetime import timedelta
 from djadhere.utils import get_active_filter
 from adhesions.models import Adhesion
 from banking.models import PaymentUpdate
-from .models import Service, ServiceType, IPPrefix, IPResource, Route, Tunnel, \
+from .models import Service, ServiceType, IPPrefix, IPResource, IPResourceState, \
                     ServiceAllocation, Antenna, AntennaAllocation, Allocation, \
-                    Switch, Port
+                    Route, Tunnel, Switch, Port
 from .utils.notifications import notify_allocation
-from .forms import AntennaForm
+from .forms import AntennaForm, StopAllocationForm
 
 
 ### Filters
@@ -67,13 +67,15 @@ class ResourcePingFilter(admin.SimpleListFilter):
 
     def queryset(self, request, queryset):
         if self.value() == 'up':
-            return queryset.filter(up=True)
+            return queryset.filter(last_state__state=IPResourceState.STATE_UP)
         if self.value() == 'down':
-            return queryset.filter(models.Q(up__isnull=True) | models.Q(up=False))
+            return queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKNOWN
         if self.value() == 'down-since':
-            return queryset.filter(up=False)
+            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP)
+            return queryset.filter(last_time_up__isnull=False)
         if self.value() == 'never-up':
-            return queryset.filter(up__isnull=True)
+            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKWON
+            return queryset.filter(last_time_up__isnull=True)
 
 
 class ActiveServiceFilter(admin.SimpleListFilter):
@@ -204,14 +206,17 @@ class AllocationInline(admin.TabularInline):
         return False
 
 
+class NewAllocationMixin:
+    verbose_name_plural = 'Nouvelle allocation'
+    max_num = 1
+
+    def get_queryset(self, request):
+        return super().get_queryset(request).model.objects.none()
+
+
 class ActiveAllocationMixin:
     verbose_name_plural = 'Allocations actives'
-
-    def get_max_num(self, request, obj=None, **kwargs):
-        existing = obj.allocations.count() if obj else 0
-        # pour simplifier la validation, on ajoute qu’une allocation à la fois
-        # il faudrait surcharger la méthode clean du formset pour supprimer cette limite
-        return existing + 1
+    max_num = 0
 
     def get_queryset(self, request):
         return super().get_queryset(request).filter(get_active_filter())
@@ -237,32 +242,56 @@ class ServiceAllocationMixin:
         return qs
 
 
-class AntennaAllocationMixin:
-    model = AntennaAllocation
-    fields = ('id', 'antenna', 'resource', 'start', 'end')
-    raw_id_fields = ('resource',)
-    autocomplete_fields = ('antenna',)
+#class AntennaAllocationMixin:
+#    model = AntennaAllocation
+#    fields = ('id', 'antenna', 'resource', 'start', 'end')
+#    raw_id_fields = ('resource',)
+#    autocomplete_fields = ('antenna',)
+#
+#    def get_queryset(self, request):
+#        qs = super().get_queryset(request)
+#        qs = qs.select_related('antenna')
+#        return qs
 
-    def get_queryset(self, request):
-        qs = super().get_queryset(request)
-        qs = qs.select_related('antenna')
-        return qs
+
+class NewServiceAllocationInline(ServiceAllocationMixin, NewAllocationMixin, AllocationInline):
+    fields = ('id', 'service', 'resource', 'route',)
 
 
 class ActiveServiceAllocationInline(ServiceAllocationMixin, ActiveAllocationMixin, AllocationInline):
-    pass
+    fields = ('id', 'service', 'resource', 'route', 'start', 'stop',)
+    readonly_fields = ('service', 'start', 'resource', 'stop',)
+
+    def stop(self, obj):
+        return format_html('<a href="{}" class="deletelink">Terminer</a>', reverse('admin:stop-allocation', kwargs={'resource': obj.resource.ip}))
+    stop.short_description = 'Terminer l’allocation'
 
 
 class InactiveServiceAllocationInline(ServiceAllocationMixin, InactiveAllocationMixin, AllocationInline):
-    pass
+    fields = ('id', 'service', 'resource', 'route', 'start', 'end')
+    readonly_fields = ('service', 'resource', 'route', 'start', 'end')
 
 
-class ActiveAntennaAllocationInline(AntennaAllocationMixin, ActiveAllocationMixin, AllocationInline):
-    pass
+#class ActiveAntennaAllocationInline(AntennaAllocationMixin, ActiveAllocationMixin, AllocationInline):
+#    pass
 
 
-class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocationMixin, AllocationInline):
-    pass
+#class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocationMixin, AllocationInline):
+#    pass
+
+
+class IPResourceStateInline(admin.TabularInline):
+    model = IPResourceState
+    verbose_name_plural = 'Historique des derniers changements d’état'
+    fields = ['date']
+    readonly_fields = ['date']
+    ordering = ['-date']
+
+    def has_add_permission(self, request):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
 
 
 class PortInline(admin.TabularInline):
@@ -292,15 +321,6 @@ class ServicePortInline(PortInline):
     readonly_fields = ('switch', 'port', 'up',)
 
 
-### Actions
-
-def ends_resource(resource, request, queryset):
-    now = timezone.now()
-    queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
-    # TODO: send mail
-ends_resource.short_description = 'Terminer les allocations sélectionnées'
-
-
 ### ModelAdmin
 
 class ServiceAdmin(admin.ModelAdmin):
@@ -335,7 +355,11 @@ class ServiceAdmin(admin.ModelAdmin):
         inlines = []
         if obj and obj.ports.exists():
             inlines += [ServicePortInline]
-        inlines += [ActiveServiceAllocationInline, InactiveServiceAllocationInline]
+        inlines += [NewServiceAllocationInline]
+        if obj and obj.active_allocations.exists():
+            inlines += [ActiveServiceAllocationInline]
+        if obj and obj.inactive_allocations.exists():
+            inlines += [InactiveServiceAllocationInline]
         return [inline(self.model, self.admin_site) for inline in inlines]
 
     def get_actions(self, request):
@@ -385,35 +409,41 @@ class IPResourceAdmin(admin.ModelAdmin):
     )
     search_fields = ('=ip', 'notes',)
     actions = ['contact_ip_owners']
+    ordering = ['ip']
+    inlines = [ IPResourceStateInline ]
 
     def get_fields(self, request, obj=None):
         return self.get_readonly_fields(request, obj)
 
     def get_readonly_fields(self, request, obj=None):
         fields = ['ip']
-        if obj and obj.reserved:
-            fields += ['reserved']
-        if obj and not obj.in_use:
-            fields += ['last_use']
-        if obj and obj.last_time_up and obj.last_check:
-            fields += ['last_time_up', 'last_check']
-        if obj.category == IPResource.CATEGORY_PUBLIC:
-            fields += ['password']
-        if obj and obj.checkmk_label:
-            fields += ['checkmk']
-        if obj and obj.notes:
-            fields += ['notes']
+        if obj:
+            if obj.reserved:
+                fields += ['reserved']
+            if not obj.in_use:
+                fields += ['last_use']
+            fields += ['last_state']
+            if obj.last_state.state != IPResourceState.STATE_UP:
+                fields += ['last_time_up']
+            if obj.category == IPResource.CATEGORY_PUBLIC:
+                fields += ['password']
+            if obj.checkmk_label:
+                fields += ['checkmk']
+            if obj.notes:
+                fields += ['notes']
         return fields
 
     def get_inline_instances(self, request, obj=None):
-        if obj:
-            if obj.category == 0:
-                inlines = (ActiveServiceAllocationInline, InactiveServiceAllocationInline,)
-            elif obj.category == 1:
-                inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
-        else:
-            inlines = ()
-        return [inline(self.model, self.admin_site) for inline in inlines]
+        super_inlines = super().get_inline_instances(request, obj)
+        inlines = []
+        if obj and obj.category == IPResource.CATEGORY_PUBLIC:
+            if obj.allocations.filter(get_active_filter()).exists():
+                inlines += [ActiveServiceAllocationInline]
+            else:
+                inlines += [NewServiceAllocationInline]
+            if obj.allocations.exclude(get_active_filter()).exists():
+                inlines += [InactiveServiceAllocationInline]
+        return [inline(self.model, self.admin_site) for inline in inlines] + super_inlines
 
     def get_queryset(self, request):
         qs = super().get_queryset(request)
@@ -426,13 +456,12 @@ class IPResourceAdmin(admin.ModelAdmin):
                     default=None,
                 ))
         qs = qs.annotate(
-                up=models.Case(
-                    models.When(last_check__isnull=False, last_time_up__isnull=False, last_time_up=models.F('last_check'), then=True),
-                    models.When(last_check__isnull=False, last_time_up__isnull=False, then=False),
+                downtime=models.Case(
+                    models.When(last_state__state=IPResourceState.STATE_UP, then=models.F('last_state__date')-models.Value(now)),
+                    models.When(last_state__state=IPResourceState.STATE_DOWN, then=models.Value(now)-models.F('last_time_up')),
                     default=None,
-                    output_field=models.NullBooleanField(),
+                    output_field=models.DurationField(),
                 ))
-        qs = qs.annotate(downtime=models.F('last_check') - models.F('last_time_up'))
         qs = qs.annotate(
                 route=models.Case(
                     models.When(
@@ -461,17 +490,19 @@ class IPResourceAdmin(admin.ModelAdmin):
     last_use.admin_order_field = 'last_use'
 
     def ping(self, obj):
-        if obj.up:
+        if obj.last_state.state == IPResourceState.STATE_UP:
             label = 'UP'
-        elif obj.last_time_up:
-            label = 'dernier ping : ' + naturaltime(obj.last_time_up)
         else:
-            label = 'DOWN'
+            if obj.last_time_up:
+                label = 'dernier ping : ' + naturaltime(obj.last_time_up)
+            else:
+                label = 'DOWN'
         if obj.checkmk_url:
             return format_html('<a href="{}">{}</a>', obj.checkmk_url, label)
         else:
             return label
     ping.short_description = 'ping'
+    #ping.admin_order_field = 'last_state__date'
     ping.admin_order_field = 'downtime'
 
     def route(self, obj):
@@ -495,6 +526,35 @@ class IPResourceAdmin(admin.ModelAdmin):
         return HttpResponseRedirect(reverse('admin:contact-adherents') + "?pk=%s" % pk)
     contact_ip_owners.short_description = 'Contacter les adhérents'
 
+    def stop_allocation(self, request, resource):
+        resource = self.get_object(request, resource)
+        allocation = resource.allocations.filter(get_active_filter()).first()
+        if not allocation:  # L’IP n’est pas allouée
+            return HttpResponseRedirect(reverse('admin:services_ipresource_change', args=[resource.pk]))
+        form = StopAllocationForm(request.POST or None)
+        if request.method == 'POST' and form.is_valid():
+            self.message_user(request, 'Allocation stoppée.')
+            allocation.end = timezone.now()
+            allocation.save()
+            notify_allocation(request, allocation)
+            # Il faudrait rajouter un redirect dans l’URL pour rediriger vers l’IP ou le Service
+            return HttpResponseRedirect(reverse('admin:services_ipresource_change', args=[resource.pk]))
+        context = self.admin_site.each_context(request)
+        context.update({
+            'opts': self.model._meta,
+            'title': 'Stopper une allocation',
+            'object': resource,
+            'media': self.media,
+            'form': form,
+        })
+        return TemplateResponse(request, "admin/services/ipresource/stop_allocation.html", context)
+
+    def get_urls(self):
+        my_urls = [
+            path('<resource>/stop/', self.admin_site.admin_view(self.stop_allocation), name='stop-allocation'),
+        ]
+        return my_urls + super().get_urls()
+
     def get_actions(self, request):
         actions = super().get_actions(request)
         if 'delete_selected' in actions:
@@ -508,6 +568,25 @@ class IPResourceAdmin(admin.ModelAdmin):
         return False
 
 
+class IPResourceStateAdmin(admin.ModelAdmin):
+    list_display = ('ip', 'date', 'state',)
+
+    def get_actions(self, request):
+        actions = super().get_actions(request)
+        if 'delete_selected' in actions:
+            del actions['delete_selected']
+        return actions
+
+    def has_add_permission(self, request, obj=None):
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+
 class RouteAdmin(admin.ModelAdmin):
     list_display = ('name',)
     search_fields = ('name',)
@@ -590,7 +669,7 @@ class ActiveAntennaLayer(GeoJSONLayerView):
 
 
 class AntennaAdmin(admin.ModelAdmin):
-    inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
+    #inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
     list_filter = (
         AntennaPrefixFilter,
         AntennaPositionFilter,

+ 4 - 0
services/forms.py

@@ -4,6 +4,10 @@ from django.contrib.gis.geos import Point
 from .models import Antenna
 
 
+class StopAllocationForm(forms.Form):
+    pass
+
+
 class AntennaForm(forms.ModelForm):
     longitude = forms.FloatField(
         min_value=-180,

+ 28 - 0
services/migrations/0052_auto_20190219_2146.py

@@ -0,0 +1,28 @@
+# Generated by Django 2.1.1 on 2019-02-19 20:46
+
+from django.db import migrations, models
+from django.utils import timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0051_auto_20180602_1346'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='IPResourceState',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('date', models.DateTimeField(default=timezone.now)),
+                ('state', models.IntegerField(choices=[(0, 'DOWN'), (1, 'UP'), (2, 'Inconnu')])),
+                ('ip', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='state_set', to='services.IPResource')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='ipresource',
+            name='last_state',
+            field=models.ForeignKey(null=True, on_delete=models.deletion.PROTECT, to='services.IPResourceState', related_name='+'),
+        ),
+    ]

+ 51 - 0
services/migrations/0053_auto_20190219_2147.py

@@ -0,0 +1,51 @@
+# Generated by Django 2.1.1 on 2019-02-19 20:47
+
+from django.db import migrations
+from django.utils import timezone
+
+from datetime import timedelta
+
+
+def forward(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
+    IPResource = apps.get_model('services', 'IPResource')
+    IPResourceState = apps.get_model('services', 'IPResourceState')
+    now = timezone.now()
+    for ip in IPResource.objects.all():
+        if ip.last_check:
+            if ip.last_time_up:
+                if ip.last_check == ip.last_time_up: # UP
+                    ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_time_up, state=1) # UP
+                    ip.save()
+                else: # DOWN but UP some time before
+                    ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_time_up, state=0) # DOWN
+                    ip.save()
+            else: # Always DOWN
+                ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_check, state=0) # DOWN
+                ip.save()
+        else:
+            ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=2) # UNKNOWN
+            ip.save()
+
+
+def backward(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
+    IPResource = apps.get_model('services', 'IPResource')
+    IPResourceState = apps.get_model('services', 'IPResourceState')
+    for ip in IPResource.objects.all():
+        if ip.last_state.state != 2:
+            ip.last_check = ip.last_state.date
+            if ip.last_state.state == 1:
+                ip.last_time_up = ip.last_check
+            ip.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0052_auto_20190219_2146'),
+    ]
+
+    operations = [
+		migrations.RunPython(forward, backward)
+    ]

+ 22 - 0
services/migrations/0054_auto_20190219_2206.py

@@ -0,0 +1,22 @@
+# Generated by Django 2.1.1 on 2019-02-19 21:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0053_auto_20190219_2147'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='ipresource',
+            name='last_check',
+        ),
+        migrations.AlterField(
+            model_name='ipresource',
+            name='last_state',
+            field=models.ForeignKey(on_delete=models.deletion.PROTECT, to='services.IPResourceState', related_name='+'),
+        ),
+    ]

+ 24 - 3
services/models.py

@@ -102,16 +102,16 @@ class IPResource(models.Model):
     category = models.IntegerField(choices=CATEGORIES, verbose_name='catégorie')
     notes = models.TextField(blank=True, default='')
     checkmk_label = models.CharField(max_length=128, blank=True, default='')
+    last_state = models.ForeignKey("IPResourceState", on_delete=models.PROTECT, related_name='+', verbose_name='dernier état')
     last_time_up = models.DateTimeField(null=True, blank=True, verbose_name='Dernière réponse au ping')
-    last_check = models.DateTimeField(null=True, blank=True, verbose_name='Dernier contrôle')
 
     objects = IPResourceManager()
 
     @property
     def allocations(self):
-        if self.category == 0:
+        if self.category == self.CATEGORY_PUBLIC:
             return self.service_allocations
-        if self.category == 1:
+        if self.category == self.CATEGORY_ANTENNA:
             return self.antenna_allocations
 
     @property
@@ -135,6 +135,23 @@ class IPResource(models.Model):
         return str(self.ip)
 
 
+class IPResourceState(models.Model):
+    STATE_DOWN = 0
+    STATE_UP = 1
+    STATE_UNKNOWN = 2
+    STATE_CHOICES = (
+        (STATE_DOWN, 'DOWN'),
+        (STATE_UP, 'UP'),
+        (STATE_UNKNOWN, 'Inconnu'),
+    )
+    ip = models.ForeignKey(IPResource, on_delete=models.CASCADE, related_name='state_set')
+    date = models.DateTimeField(default=timezone.now)
+    state = models.IntegerField(choices=STATE_CHOICES)
+
+    def __str__(self):
+        return self.get_state_display()
+
+
 class ServiceType(models.Model):
     name = models.CharField(max_length=64, verbose_name='Nom', unique=True)
     contact = models.CharField(max_length=64, verbose_name='Contact en cas de problème', blank=True, default='')
@@ -181,6 +198,10 @@ class Service(models.Model):
     def active_allocations(self):
         return self.allocations.filter(get_active_filter())
 
+    @property
+    def inactive_allocations(self):
+        return self.allocations.exclude(get_active_filter())
+
     def get_absolute_url(self):
         return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=(self.pk,))
 

+ 31 - 0
services/templates/admin/services/ipresource/stop_allocation.html

@@ -0,0 +1,31 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_urls static %}
+
+{% block extrahead %}
+    {{ block.super }}
+    {{ media }}
+    <script type="text/javascript" src="{% static 'admin/js/cancel.js' %}"></script>
+{% endblock %}
+
+{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
+&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
+&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
+&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
+&rsaquo; Stopper l’allocation
+</div>
+{% endblock %}
+
+{% block content %}
+    <p>Voulez-vous vraiment stopper l’allocation de l’IP « {{ object }} » ?</p>
+    <form method="post">{% csrf_token %}
+    <div>
+    <input type="hidden" name="post" value="yes">
+    <input type="submit" value="{% trans "Yes, I'm sure" %}">
+    <a href="#" class="button cancel-link">{% trans "No, take me back" %}</a>
+    </div>
+    </form>
+{% endblock %}

+ 8 - 1
services/templates/services/service_detail.html

@@ -25,7 +25,14 @@
             IP allouée{{ service.active_allocations.count|pluralize }} :
             {% for allocation in service.active_allocations.all %}
             {% if forloop.first %}<ul>{% endif %}
-                <li>{{ allocation.resource }}{% if allocation.resource.last_time_up %} (dernière réponse au ping : {{ allocation.resource.last_time_up|naturaltime }}){% endif %}</li>
+                <li>
+                    {{ allocation.resource }}
+                    {% if allocation.resource.last_state.get_state_display == 'UP' %}
+                        (UP)
+                    {% elif allocation.resource.last_time_up %}
+                        (dernière réponse au ping : {{ allocation.resource.last_time_up|naturaltime }})
+                    {% endif %}
+                </li>
             {% if forloop.last %}</ul>{% endif %}
             {% empty %}
             aucune IP allouée

+ 40 - 18
services/utils/fastpinger.py

@@ -1,18 +1,22 @@
 from django.db.models import Q, F
 from django.utils import timezone
 
-from services.models import IPResource
+from services.models import IPResource, IPResourceState, ServiceAllocation
 
 import re
 from ipaddress import IPv4Address, AddressValueError
+import logging
+from itertools import groupby
+
+
+logger = logging.getLogger(__name__)
 
 
 def fastpinger_update(f):
+    now = timezone.now()
+
     regex = re.compile('^(?P<ip>[0-9.]+)[ ]+: (?P<p1>([0-9]+.[0-9]+|-)) (?P<p2>([0-9]+.[0-9]+|-))'
                        ' (?P<p3>([0-9]+.[0-9]+|-)) (?P<p4>([0-9]+.[0-9]+|-)) (?P<p5>([0-9]+.[0-9]+|-))$')
-    up_count = IPResource.objects.filter(last_check__isnull=False).filter(last_time_up=F('last_check')).count()
-    down_count = IPResource.objects.filter(last_check__isnull=False).exclude(last_time_up=F('last_check')).count()
-    unknown_count = IPResource.objects.filter(last_check__isnull=True).count()
 
     up, down, unknown = set(), set(), set()
     for line in f:
@@ -35,21 +39,39 @@ def fastpinger_update(f):
 
     up = up - down # suppression des doublons
 
-    up = IPResource.objects.filter(ip__in=up)
-    down = IPResource.objects.filter(ip__in=down)
-    unknown = IPResource.objects.exclude(Q(pk__in=up) | Q(pk__in=down))
-
-    up.exclude(last_time_up=F('last_check')).count()
-    down.filter(last_time_up=F('last_check')).count()
+    leaves = {
+        IPResourceState.STATE_DOWN: 0,
+        IPResourceState.STATE_UP: 0,
+        IPResourceState.STATE_UNKNOWN: 0,
+    }
 
-    now = timezone.now()
+    become_down = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_DOWN).filter(ip__in=down)
+    for ip in become_down:
+        leaves[ip.last_state.state] += 1
+        if ip.last_state.state == IPResourceState.STATE_UP:
+            ip.last_time_up = now
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_DOWN)
+        ip.save()
+    become_up = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_UP).filter(ip__in=up)
+    for ip in become_up:
+        leaves[ip.last_state.state] += 1
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_UP)
+        ip.save()
+    become_unknown = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_UNKNOWN).exclude(Q(pk__in=up) | Q(pk__in=down))
+    for ip in become_unknown:
+        leaves[ip.last_state.state] += 1
+        if ip.last_state.state == IPResourceState.STATE_UP:
+            ip.last_time_up = now
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_UNKNOWN)
+        ip.save()
 
-    up.update(last_time_up=now, last_check=now)
-    down.update(last_check=now)
 
-    upped = len(up) - up_count
-    downed = len(down) - down_count
-    unknowned = len(unknown) - unknown_count
+    down_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_DOWN).count()
+    up_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_UP).count()
+    unknown_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_UNKNOWN).count()
 
-    return "UP: %d (%+d), DOWN: %d (%+d), UNKNOWN: %d (%+d)" \
-            % (len(up), upped, len(down), downed, len(unknown), unknowned)
+    stats = "UP: %d (-%d+%d), DOWN: %d (-%d+%d), UNKNOWN: %d (-%d+%d)" \
+            % (up_count, leaves[IPResourceState.STATE_UP], len(become_up), \
+               down_count, leaves[IPResourceState.STATE_DOWN], len(become_down), \
+               unknown_count, leaves[IPResourceState.STATE_UNKNOWN], len(become_unknown))
+    return stats

+ 7 - 3
services/utils/notifications.py

@@ -4,6 +4,7 @@ from django.conf import settings
 from djadhere.utils import send_notification, send_web_notification
 
 
+# À simplifier : seul la route peut changer maintenant
 def notify_allocation(request, new_alloc, old_alloc=None):
     fields = ['resource', 'service', 'route', 'start', 'end', 'notes']
 
@@ -32,13 +33,16 @@ def notify_allocation(request, new_alloc, old_alloc=None):
     message += '\n\nVoir : ' + url
 
     if old_alloc and diff:
-        sujet = 'Modification d’une allocation'
+        sujet = 'Modification d’allocation'
     elif not old_alloc:
-        sujet = 'Nouvelle allocation'
+        if new_alloc.end:
+            sujet = 'Fin d’allocation'
+        else:
+            sujet = 'Nouvelle allocation'
     else:
         sujet = None
 
     if sujet:
-        sujet += ' ADT%d' % new_alloc.service.adhesion.pk
+        sujet += ' {} sur {}'.format(new_alloc.resource, new_alloc.route)
         send_notification(sujet, message, settings.ALLOCATIONS_EMAILS, cc=[benevole])
         send_web_notification(sujet, message)