From c2f3ae77fd8107439378dec37aa40647da96f1d1 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:18:32 +0100
Subject: [PATCH 01/66] Notifications: application structure

---
 fir_notifications/__init__.py              |  1 +
 fir_notifications/apps.py                  | 10 ++++++++++
 fir_notifications/migrations/__init__.py   |  0
 fir_notifications/models.py                |  3 +++
 fir_notifications/templatetags/__init__.py |  0
 fir_notifications/urls.py                  |  6 ++++++
 fir_notifications/views.py                 |  0
 7 files changed, 20 insertions(+)
 create mode 100644 fir_notifications/__init__.py
 create mode 100644 fir_notifications/apps.py
 create mode 100644 fir_notifications/migrations/__init__.py
 create mode 100644 fir_notifications/models.py
 create mode 100644 fir_notifications/templatetags/__init__.py
 create mode 100644 fir_notifications/urls.py
 create mode 100644 fir_notifications/views.py

diff --git a/fir_notifications/__init__.py b/fir_notifications/__init__.py
new file mode 100644
index 00000000..ac79acc6
--- /dev/null
+++ b/fir_notifications/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'fir_notifications.apps.NotificationsConfig'
diff --git a/fir_notifications/apps.py b/fir_notifications/apps.py
new file mode 100644
index 00000000..f14bf75d
--- /dev/null
+++ b/fir_notifications/apps.py
@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class NotificationsConfig(AppConfig):
+    name = 'fir_notifications'
+    verbose_name = _('Notifications')
+
+    def ready(self):
+        pass
diff --git a/fir_notifications/migrations/__init__.py b/fir_notifications/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fir_notifications/models.py b/fir_notifications/models.py
new file mode 100644
index 00000000..72b97ff3
--- /dev/null
+++ b/fir_notifications/models.py
@@ -0,0 +1,3 @@
+from __future__ import unicode_literals
+
+from django.db import models
\ No newline at end of file
diff --git a/fir_notifications/templatetags/__init__.py b/fir_notifications/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fir_notifications/urls.py b/fir_notifications/urls.py
new file mode 100644
index 00000000..83250604
--- /dev/null
+++ b/fir_notifications/urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls import url
+
+
+urlpatterns = [
+
+]
\ No newline at end of file
diff --git a/fir_notifications/views.py b/fir_notifications/views.py
new file mode 100644
index 00000000..e69de29b

From 0d0bbced43de73d0f5f05dd3214ca378836d6c2f Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:28:13 +0100
Subject: [PATCH 02/66] Notifications: event and method registry

---
 fir_notifications/registry.py | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)
 create mode 100644 fir_notifications/registry.py

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
new file mode 100644
index 00000000..16b334b7
--- /dev/null
+++ b/fir_notifications/registry.py
@@ -0,0 +1,34 @@
+from collections import OrderedDict
+
+
+class Notifications(object):
+    def __init__(self):
+        self.methods = OrderedDict()
+        self.events = OrderedDict()
+
+    def register_method(self, method, name=None, verbose_name=None):
+        if not method.server_configured:
+            return
+        if name is not None:
+            method.name = name
+        if verbose_name is not None:
+            method.verbose_name = verbose_name
+        if not method.verbose_name:
+            method.verbose_name = method.name
+        self.methods[method.name] = method
+
+    def register_event(self, name, signal, model, callback, verbose_name=None):
+        if verbose_name is None:
+            verbose_name = name
+        self.events[name] = verbose_name
+
+        signal.connect(callback, sender=model, dispatch_uid="fir_notifications.{}".format(name))
+
+    def get_event_choices(self):
+        return self.events.items()
+
+    def get_method_choices(self):
+        return [(obj.name, obj.verbose_name) for obj in self.methods.values()]
+
+
+registry = Notifications()
\ No newline at end of file

From 1edf7dad9a353900eb98c93f840b9b2edf375dc7 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:51:24 +0100
Subject: [PATCH 03/66] Notifications: models for notification method
 configuration and notification template

---
 fir_notifications/models.py | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index 72b97ff3..9a6c5fea 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -1,3 +1,38 @@
 from __future__ import unicode_literals
 
-from django.db import models
\ No newline at end of file
+from django.db import models
+from django.conf import settings
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+
+from fir_notifications.registry import registry
+
+
+@python_2_unicode_compatible
+class MethodConfiguration(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='method_preferences', verbose_name=_('user'))
+    key = models.CharField(max_length=60, choices=registry.get_method_choices(), verbose_name=_('method'))
+    value = models.TextField(verbose_name=_('configuration'))
+
+    def __str__(self):
+        return "{user}: {method} configuration".format(user=self.user, method=self.key)
+
+    class Meta:
+        verbose_name = _('method configuration')
+        verbose_name_plural = _('method configurations')
+        unique_together = (("user", "key"),)
+        index_together = ["user", "key"]
+
+
+class NotificationTemplate(models.Model):
+    event = models.CharField(max_length=60, choices=registry.get_event_choices(), verbose_name=_('event'))
+    business_lines = models.ManyToManyField('incidents.BusinessLine', related_name='+', blank=True,
+                                            verbose_name=_('business line'))
+    subject = models.CharField(max_length=200, blank=True, default="", verbose_name=_('subject'))
+    short_description = models.TextField(blank=True, default="", verbose_name=_('short description'))
+    description = models.TextField(blank=True, default="", verbose_name=_('description'))
+
+    class Meta:
+        verbose_name = _('notification template')
+        verbose_name_plural = _('notification templates')
+

From eeed03d2185b75e5494e5b1b84231d5a729e733c Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:52:00 +0100
Subject: [PATCH 04/66] Notifications: method configuration form

---
 fir_notifications/forms.py | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)
 create mode 100644 fir_notifications/forms.py

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
new file mode 100644
index 00000000..8061bace
--- /dev/null
+++ b/fir_notifications/forms.py
@@ -0,0 +1,23 @@
+import json
+
+from django.forms import forms
+
+from fir_notifications.models import MethodConfiguration
+
+
+class MethodConfigurationForm(forms.Form):
+    def __init__(self, *args, **kwargs):
+        self.method = kwargs.pop('method')
+        self.user = kwargs.pop('user', None)
+        super(MethodConfigurationForm, self).__init__(*args, **kwargs)
+        for option_id, option_field in self.method.options.items():
+            self.fields[option_id] = option_field
+        self.title = _("Configure %(method)s" % {'method': self.method.verbose_name})
+
+    def save(self, *args, **kwargs):
+        if self.user is None:
+            return None
+        json_value = json.dumps(self.cleaned_data)
+        config, created = MethodConfiguration.objects.update_or_create(user=self.user, key=self.method.name,
+                                                                       defaults={'value': json_value})
+        return config

From 996bc72555f9b3448f38d0f400eb126e6187f006 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:55:49 +0100
Subject: [PATCH 05/66] Notifications: template form

---
 fir_notifications/forms.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 8061bace..9cd60cd6 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -1,7 +1,8 @@
 import json
 
-from django.forms import forms
+from django import forms
 
+from fir_notifications.registry import registry
 from fir_notifications.models import MethodConfiguration
 
 
@@ -21,3 +22,10 @@ def save(self, *args, **kwargs):
         config, created = MethodConfiguration.objects.update_or_create(user=self.user, key=self.method.name,
                                                                        defaults={'value': json_value})
         return config
+
+
+class NotificationTemplateForm(forms.ModelForm):
+    event = forms.ChoiceField(choices=registry.get_event_choices())
+
+    class Meta:
+        fields = '__all__'

From 1fe944344b42148002f3640fb5815a161d2c25ae Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 12:56:52 +0100
Subject: [PATCH 06/66] Notifications: template admin

---
 fir_notifications/admin.py | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)
 create mode 100644 fir_notifications/admin.py

diff --git a/fir_notifications/admin.py b/fir_notifications/admin.py
new file mode 100644
index 00000000..76a22461
--- /dev/null
+++ b/fir_notifications/admin.py
@@ -0,0 +1,25 @@
+from django.contrib import admin
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _, pgettext_lazy
+
+from fir_plugins.admin import MarkdownModelAdmin
+from fir_notifications.models import MethodConfiguration, NotificationTemplate
+from fir_notifications.forms import NotificationTemplateForm
+
+
+class NotificationTemplateAdmin(MarkdownModelAdmin):
+    markdown_fields = ('description', 'short_description')
+    form = NotificationTemplateForm
+    list_display = ('event', 'business_lines_list')
+
+    def business_lines_list(self, obj):
+        bls = obj.business_lines.all()
+        if bls.count():
+            return ', '.join([bl.name for bl in bls])
+        return pgettext_lazy('business lines', 'All')
+    business_lines_list.short_description = _('Business lines')
+
+
+admin.site.register(NotificationTemplate, NotificationTemplateAdmin)
+if settings.DEBUG:
+    admin.site.register(MethodConfiguration)

From ffb0e5d31138431656dcf1375e60c7721fc57deb Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 13:01:02 +0100
Subject: [PATCH 07/66] Notifications: DB migration for method configuration
 and template

---
 fir_notifications/migrations/0001_initial.py | 56 ++++++++++++++++++++
 1 file changed, 56 insertions(+)
 create mode 100644 fir_notifications/migrations/0001_initial.py

diff --git a/fir_notifications/migrations/0001_initial.py b/fir_notifications/migrations/0001_initial.py
new file mode 100644
index 00000000..126b8683
--- /dev/null
+++ b/fir_notifications/migrations/0001_initial.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.9 on 2017-01-14 13:00
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('incidents', '0009_add_incicent_permissions'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='MethodConfiguration',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('key', models.CharField(max_length=60, verbose_name='method')),
+                ('value', models.TextField(verbose_name='configuration')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='method_preferences', to=settings.AUTH_USER_MODEL, verbose_name='user')),
+            ],
+            options={
+                'verbose_name': 'method configuration',
+                'verbose_name_plural': 'method configurations',
+            },
+        ),
+        migrations.CreateModel(
+            name='NotificationTemplate',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('event', models.CharField(max_length=60, verbose_name='event')),
+                ('subject', models.CharField(blank=True, default='', max_length=200, verbose_name='subject')),
+                ('short_description', models.TextField(blank=True, default='', verbose_name='short description')),
+                ('description', models.TextField(blank=True, default='', verbose_name='description')),
+                ('business_lines', models.ManyToManyField(blank=True, related_name='_notificationtemplate_business_lines_+', to='incidents.BusinessLine', verbose_name='business line')),
+            ],
+            options={
+                'verbose_name': 'notification template',
+                'verbose_name_plural': 'notification templates',
+            },
+        ),
+        migrations.AlterUniqueTogether(
+            name='methodconfiguration',
+            unique_together=set([('user', 'key')]),
+        ),
+        migrations.AlterIndexTogether(
+            name='methodconfiguration',
+            index_together=set([('user', 'key')]),
+        ),
+    ]

From aa1ca012921475f0c8a2ec9e75b4b8f04060403b Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 13:03:52 +0100
Subject: [PATCH 08/66] Notifications: user preference model

---
 fir_notifications/models.py | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index 9a6c5fea..816a7081 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -36,3 +36,22 @@ class Meta:
         verbose_name = _('notification template')
         verbose_name_plural = _('notification templates')
 
+
+@python_2_unicode_compatible
+class NotificationPreference(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notification_preferences', verbose_name=_('user'))
+    event = models.CharField(max_length=60, verbose_name=_('event'))
+    method = models.CharField(max_length=60, verbose_name=_('method'))
+    business_lines = models.ManyToManyField('incidents.BusinessLine', related_name='+', blank=True,
+                                            verbose_name=_('business lines'))
+
+    def __str__(self):
+        return "{user}: {event} notification preference for {method}".format(user=self.user,
+                                                                             event=self.event,
+                                                                             method=self.method)
+
+    class Meta:
+        verbose_name = _('notification preference')
+        verbose_name_plural = _('notification preferences')
+        unique_together = (("user", "event", "method"),)
+        index_together = ["user", "event", "method"]

From 673e8551b620641be2ed28a182d279aac652fe33 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 13:04:41 +0100
Subject: [PATCH 09/66] Notifications: DB migration for user preference

---
 .../migrations/0002_user_preference.py        | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 fir_notifications/migrations/0002_user_preference.py

diff --git a/fir_notifications/migrations/0002_user_preference.py b/fir_notifications/migrations/0002_user_preference.py
new file mode 100644
index 00000000..4e4a281b
--- /dev/null
+++ b/fir_notifications/migrations/0002_user_preference.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.9 on 2017-01-14 13:03
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('incidents', '0009_add_incicent_permissions'),
+        ('fir_notifications', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='NotificationPreference',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('event', models.CharField(max_length=60, verbose_name='event')),
+                ('method', models.CharField(max_length=60, verbose_name='method')),
+                ('business_lines', models.ManyToManyField(blank=True, related_name='_notificationpreference_business_lines_+', to='incidents.BusinessLine', verbose_name='business lines')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_preferences', to=settings.AUTH_USER_MODEL, verbose_name='user')),
+            ],
+            options={
+                'verbose_name': 'notification preference',
+                'verbose_name_plural': 'notification preferences',
+            },
+        ),
+        migrations.AlterUniqueTogether(
+            name='notificationpreference',
+            unique_together=set([('user', 'event', 'method')]),
+        ),
+        migrations.AlterIndexTogether(
+            name='notificationpreference',
+            index_together=set([('user', 'event', 'method')]),
+        ),
+    ]

From f38f137f7e75346156ede8566e4f27a99e1b6be0 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 13:15:32 +0100
Subject: [PATCH 10/66] Notifications: method base class

---
 fir_notifications/methods/__init__.py | 105 ++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)
 create mode 100644 fir_notifications/methods/__init__.py

diff --git a/fir_notifications/methods/__init__.py b/fir_notifications/methods/__init__.py
new file mode 100644
index 00000000..cd8a6519
--- /dev/null
+++ b/fir_notifications/methods/__init__.py
@@ -0,0 +1,105 @@
+from django.template import Template, Context
+
+import json
+
+
+class NotificationMethod(object):
+    """
+    Base class for a notification method.
+
+    Subclass this class to create a new notification method
+    """
+    name = 'method_template'
+    verbose_name = 'Notification method template'
+    # This notification method uses the template subject
+    use_subject = False
+    # This notification method uses the template short description
+    use_short_description = False
+    # This notification method uses the template description
+    use_description = False
+    # Method configuration options (dict: index_name: form field instance)
+    options = {}
+
+    def __init__(self):
+        self.server_configured = False
+
+    def enabled(self, event, user, paths):
+        """
+        Checks if this method is enabled for an event and its business lines in the user preferences
+        """
+        from fir_notifications.models import NotificationPreference
+        try:
+            preference = NotificationPreference.objects.get(event=event, method=self.name, user=user)
+        except NotificationPreference.DoesNotExist:
+            return False
+        for bl in preference.business_lines.all():
+            if any([bl.path.startswith(path) for path in paths]):
+                return True
+        return False
+
+    @staticmethod
+    def prepare(template_object, instance, extra_context=None):
+        """
+        Renders a notification template (subject, description, short description) for a given instance
+        which fired an event
+        """
+        if extra_context is None:
+            extra_context = {}
+        extra_context.update({'instance': instance})
+        context = Context(extra_context)
+        return {
+            'subject': Template(getattr(template_object, 'subject', "")).render(context),
+            'short_description': Template(getattr(template_object, 'short_description', "")).render(context),
+            'description': Template(getattr(template_object, 'description', "")).render(context)
+        }
+
+    def _get_template(self, templates):
+        """
+        Choose the first matching template in a template list
+        """
+        for template in templates:
+            if self.use_subject and template.subject is None:
+                continue
+            if self.use_short_description and template.short_description is None:
+                continue
+            if self.use_description and template.description is None:
+                continue
+            return template
+        return None
+
+    def _get_configuration(self, user):
+        """
+        Retrieve user configuration for this method as a dict
+        """
+        from fir_notifications.models import MethodConfiguration
+        try:
+            string_config = MethodConfiguration.objects.get(user=user, key=self.name).value
+        except MethodConfiguration.DoesNotExist:
+            return {}
+        try:
+            return json.loads(string_config)
+        except:
+            return {}
+
+    def send(self, event, users, instance, paths):
+        raise NotImplementedError
+
+    def configured(self, user):
+        """
+        Checks if this method is configured for a given user
+        """
+        return self.server_configured and user.is_active
+
+    def form(self, *args, **kwargs):
+        """
+        Returns this method configuration form
+        """
+        from fir_notifications.forms import MethodConfigurationForm
+        if not len(self.options):
+            return None
+        user = kwargs.pop('user', None)
+        if user is not None:
+            kwargs['initial'] = self._get_configuration(user)
+            kwargs['user'] = user
+        kwargs['method'] = self
+        return MethodConfigurationForm(*args, **kwargs)

From 4f233a8f3c2682db8f7311b55db5a8254f363d5c Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 13:50:22 +0100
Subject: [PATCH 11/66] Notifications: celery task (as a shared task)

---
 fir_notifications/registry.py |   3 +
 fir_notifications/tasks.py    | 102 ++++++++++++++++++++++++++++++++++
 2 files changed, 105 insertions(+)
 create mode 100644 fir_notifications/tasks.py

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 16b334b7..56457a0f 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -30,5 +30,8 @@ def get_event_choices(self):
     def get_method_choices(self):
         return [(obj.name, obj.verbose_name) for obj in self.methods.values()]
 
+    def get_methods(self):
+        return self.methods.values()
+
 
 registry = Notifications()
\ No newline at end of file
diff --git a/fir_notifications/tasks.py b/fir_notifications/tasks.py
new file mode 100644
index 00000000..1b1b300d
--- /dev/null
+++ b/fir_notifications/tasks.py
@@ -0,0 +1,102 @@
+from __future__ import absolute_import
+
+from celery import shared_task
+from django.db import models
+
+from django.contrib.auth.models import User, Permission
+from incidents.models import BusinessLine
+
+_perm_id = None
+
+
+def get_perm_id():
+    global _perm_id
+    if _perm_id is not None:
+        return _perm_id
+    perm_obj = Permission.objects.get(content_type__app_label='incidents',
+                                      codename='view_incidents')
+    _perm_id = perm_obj.pk
+    return _perm_id
+
+
+def get_templates(event, business_line=None):
+    from fir_notifications.models import NotificationTemplate
+    templates = list(NotificationTemplate.objects.filter(event=event, business_lines=business_line).order_by('id'))
+    return templates
+
+
+def get_user_templates(event, business_lines):
+    global_users = User.objects.filter(
+        models.Q(groups__permissions=get_perm_id()) | models.Q(user_permissions=get_perm_id()) | models.Q(
+            is_superuser=True)).distinct()
+    global_templates = get_templates(event)
+    # User with global permission => global templates first
+    users = {user: list(global_templates) for user in global_users}
+    business_lines = {bl: bl.get_ancestors() for bl in BusinessLine.objects.filter(path__in=business_lines).order_by('depth')}
+    depth = 1
+    all_templates = {}
+    while len(business_lines):
+        for lower in business_lines.keys():
+            if lower not in all_templates:
+                all_templates[lower] = []
+            path = business_lines[lower]
+            if len(path) > depth:
+                current_bl = path[depth-1]
+                templates = get_templates(event, current_bl)
+            else:
+                templates = get_templates(event, lower)
+                business_lines.pop(lower, None)
+                current_bl = lower
+            if len(templates):
+                users_done = []
+                # User with global permission => top-down
+                for user in global_users:
+                    users[user].extend(templates)
+                    users_done.append(user)
+                role_users = User.objects.filter(accesscontrolentry__business_line=current_bl).filter(
+                                                 accesscontrolentry__role__permissions=get_perm_id()).distinct()
+                # User with bl role => this bl templates first
+                for user in role_users:
+                    users_done.append(user)
+                    if user not in users:
+                        to_add = list(templates)
+                        to_add.extend(all_templates[lower])
+                        users[user] = to_add
+                    else:
+                        users[user] = list(templates).extend(users[user])
+                # Other users => append the templates
+                for user in users:
+                    if user not in users_done:
+                        users[user].extend(templates)
+                all_templates[lower].extend(templates)
+            else:
+                role_users = User.objects.filter(accesscontrolentry__business_line=current_bl).filter(
+                                                 accesscontrolentry__role__permissions=get_perm_id()).distinct()
+                for user in role_users:
+                    if user not in users:
+                        users[user] = list(all_templates[lower])
+        depth += 1
+    # User without global permission => global templates last
+    for user in users:
+        if user not in global_users:
+            users[user].extend(global_templates)
+    return users
+
+
+@shared_task
+def handle_notification(content_type, instance, business_lines, event):
+    from fir_notifications.registry import registry
+    from django.contrib.contenttypes.models import ContentType
+    try:
+        model = ContentType.objects.get_for_id(content_type).model_class()
+    except ContentType.DoesNotExist:
+        print("Unknown content type")
+        return
+    try:
+        instance = model.objects.get(id=instance)
+    except model.DoesNotExist:
+        print("Unknown instance")
+        return
+    users = get_user_templates(event, business_lines)
+    for method in registry.get_methods():
+        method.send(event, users, instance, business_lines)

From d54f0cd2d25bfa70986d291825cd5a4ae9a8086d Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 14:12:30 +0100
Subject: [PATCH 12/66] Notifications: Fix forms.py imports

---
 fir_notifications/forms.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 9cd60cd6..440d0345 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -1,6 +1,7 @@
 import json
 
 from django import forms
+from django.utils.translation import ugettext_lazy as _
 
 from fir_notifications.registry import registry
 from fir_notifications.models import MethodConfiguration

From 1b0c4646aa2485a5130567dc984830aaaa55bd34 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 14:14:12 +0100
Subject: [PATCH 13/66] Notifications: Add method configuration view

---
 .../templates/fir_notifications/actions.html  |  4 ++
 .../fir_notifications/actions_form.html       | 54 +++++++++++++++++++
 .../plugins/user_profile_actions.html         |  4 ++
 .../templatetags/notifications.py             | 24 +++++++++
 fir_notifications/urls.py                     |  4 +-
 fir_notifications/views.py                    | 17 ++++++
 6 files changed, 106 insertions(+), 1 deletion(-)
 create mode 100644 fir_notifications/templates/fir_notifications/actions.html
 create mode 100644 fir_notifications/templates/fir_notifications/actions_form.html
 create mode 100644 fir_notifications/templates/fir_notifications/plugins/user_profile_actions.html
 create mode 100644 fir_notifications/templatetags/notifications.py

diff --git a/fir_notifications/templates/fir_notifications/actions.html b/fir_notifications/templates/fir_notifications/actions.html
new file mode 100644
index 00000000..f164cde2
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/actions.html
@@ -0,0 +1,4 @@
+{% load i18n %}
+{% for method, method_name in actions.items %}
+<li><a href="#" id="user_configure_{{method}}" data-toggle='modal' data-target='#configure_{{method}}'><i class="glyphicon glyphicon-comment"></i>{% blocktrans %}Configure {{ method_name }}{% endblocktrans %}</a></li>
+{% endfor %}
\ No newline at end of file
diff --git a/fir_notifications/templates/fir_notifications/actions_form.html b/fir_notifications/templates/fir_notifications/actions_form.html
new file mode 100644
index 00000000..a5d4fe01
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/actions_form.html
@@ -0,0 +1,54 @@
+{% load i18n %}
+{% load add_css_class %}
+
+{% for method, method_form in actions.items %}
+    <div id="configure_{{method}}" class="modal fade notification-method-form" tabindex="-1" role="dialog" aria-labelledby="configure_{{method}}_label" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <form id="configure_{{method}}_form" method="POST" action="{% url 'notifications:method_configuration' method %}">
+                    <div class="modal-header">
+                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+                        <h4 id="configure_{{method}}_label" class="modal-title">{{method_form.title}}</h4>
+                    </div>
+                    <div class="modal-body">
+                        <div>
+                            {% csrf_token %}
+                            {% for field in method_form %}
+                            <div class="form-group row">
+                                <div class="col-sm-4">
+                                    {{ field.label_tag }}
+                                </div>
+                                <div class="col-sm-8">
+                                    {{ field|add_css_class:"form-control" }}
+                                    <span class='help-block'>
+                                        {% if not field.errors and field.help_text %}
+                                            {{field.help_text}}
+                                        {% endif %}
+                                        {% for error in field.errors %}
+                                            {{error}}
+                                        {% endfor %}
+                                    </span>
+                                </div>
+                            </div>
+                            {% endfor %}
+                        </div>
+                    </div>
+
+                    <div class="modal-footer">
+                        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">{%  trans "Cancel" %}</button>
+                        <button type="submit" class="btn btn-primary">{%  trans "Save" %}</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+{% endfor %}
+<script>
+        window.onload = function(){
+            $('.notification-method-form').on('fir.form.success', function (event) {
+                console.log(this);
+                $(this).modal('hide');
+                event.stopPropagation();
+            });
+        };
+    </script>
\ No newline at end of file
diff --git a/fir_notifications/templates/fir_notifications/plugins/user_profile_actions.html b/fir_notifications/templates/fir_notifications/plugins/user_profile_actions.html
new file mode 100644
index 00000000..46f9e736
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/plugins/user_profile_actions.html
@@ -0,0 +1,4 @@
+{% load notifications %}
+
+{% notification_forms %}
+{% notification_actions %}
\ No newline at end of file
diff --git a/fir_notifications/templatetags/notifications.py b/fir_notifications/templatetags/notifications.py
new file mode 100644
index 00000000..21de55c9
--- /dev/null
+++ b/fir_notifications/templatetags/notifications.py
@@ -0,0 +1,24 @@
+from django import template
+
+from fir_notifications.registry import registry
+
+register = template.Library()
+
+
+@register.inclusion_tag('fir_notifications/actions.html')
+def notification_actions():
+    actions = {}
+    for method_name, method_object in registry.methods.items():
+        if len(method_object.options):
+            actions[method_name] = method_object.verbose_name
+    return {'actions': actions}
+
+
+@register.inclusion_tag('fir_notifications/actions_form.html', takes_context=True)
+def notification_forms(context):
+    actions = {}
+    for method_name, method_object in registry.methods.items():
+        if len(method_object.options):
+            actions[method_name] = method_object.form(user=context['user'])
+    return {'actions': actions}
+
diff --git a/fir_notifications/urls.py b/fir_notifications/urls.py
index 83250604..dbb8a939 100644
--- a/fir_notifications/urls.py
+++ b/fir_notifications/urls.py
@@ -1,6 +1,8 @@
 from django.conf.urls import url
 
+from fir_notifications import views
 
-urlpatterns = [
 
+urlpatterns = [
+    url(r'^preferences/(?P<method>[a-zA-Z0-9_]+)$', views.method_configuration, name='method_configuration'),
 ]
\ No newline at end of file
diff --git a/fir_notifications/views.py b/fir_notifications/views.py
index e69de29b..e34e44e7 100644
--- a/fir_notifications/views.py
+++ b/fir_notifications/views.py
@@ -0,0 +1,17 @@
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import redirect
+from django.views.decorators.http import require_POST
+
+from fir_notifications.registry import registry
+
+
+@require_POST
+@login_required
+def method_configuration(request, method):
+    method_object = registry.methods.get(method, None)
+    if method is None:
+        return redirect('user:profile')
+    form = method_object.form(request.POST, user=request.user)
+    if form.is_valid():
+        form.save()
+    return redirect('user:profile')
\ No newline at end of file

From d4ae90ba6fcf88ef2504cb86596676c1aab98e0f Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 14:22:07 +0100
Subject: [PATCH 14/66] Notifications: add Email method

---
 fir_notifications/forms.py            | 12 ++++
 fir_notifications/methods/__init__.py | 15 ++++-
 fir_notifications/methods/email.py    | 83 +++++++++++++++++++++++++++
 fir_notifications/registry.py         |  5 +-
 4 files changed, 113 insertions(+), 2 deletions(-)
 create mode 100644 fir_notifications/methods/email.py

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 440d0345..30743c43 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -30,3 +30,15 @@ class NotificationTemplateForm(forms.ModelForm):
 
     class Meta:
         fields = '__all__'
+
+
+class EmailMethodConfigurationForm(MethodConfigurationForm):
+    def save(self, *args, **kwargs):
+        if self.user is None or not self.user.email:
+            return None
+        try:
+            from djembe.models import Identity
+        except ImportError:
+            return None
+        config, created = Identity.objects.update_or_create(address=self.user.email, defaults=self.cleaned_data)
+        return config
diff --git a/fir_notifications/methods/__init__.py b/fir_notifications/methods/__init__.py
index cd8a6519..48a13829 100644
--- a/fir_notifications/methods/__init__.py
+++ b/fir_notifications/methods/__init__.py
@@ -1,6 +1,19 @@
+import json
+
 from django.template import Template, Context
+from django.conf import settings
 
-import json
+
+class FakeRequest(object):
+    def __init__(self):
+        self.base = settings.EXTERNAL_URL
+        if self.base.endswith('/'):
+            self.base = self.base[:-1]
+
+    def build_absolute_uri(self, location):
+        return "{}{}".format(self.base, location)
+
+request = FakeRequest()
 
 
 class NotificationMethod(object):
diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
new file mode 100644
index 00000000..bc80bafe
--- /dev/null
+++ b/fir_notifications/methods/email.py
@@ -0,0 +1,83 @@
+import markdown2
+from django import forms
+from django.conf import settings
+from django.core import mail
+from django.utils.translation import ugettext_lazy as _
+
+
+from fir_notifications.methods import NotificationMethod, request
+from fir_plugins.links import registry as link_registry
+
+
+class EmailMethod(NotificationMethod):
+    use_subject = True
+    use_description = True
+    name = 'email'
+    verbose_name = 'Email'
+
+    def __init__(self):
+        super(NotificationMethod, self).__init__()
+        if hasattr(settings, 'NOTIFICATIONS_EMAIL_FROM'):
+            self.server_configured = True
+        if 'djembe' in settings.INSTALLED_APPS:
+            self.options['certificate'] = forms.CharField(required=False,
+                                                          widget=forms.Textarea(attrs={'cols': 60, 'rows': 15}),
+                                                          help_text=_('Encryption certificate in PEM format.'))
+
+    def send(self, event, users, instance, paths):
+        from_address = settings.NOTIFICATIONS_EMAIL_FROM
+        reply_to = {}
+        if hasattr(settings, 'NOTIFICATIONS_EMAIL_REPLY_TO'):
+            reply_to = {'Reply-To': settings.NOTIFICATIONS_EMAIL_REPLY_TO,
+                        'Return-Path': settings.NOTIFICATIONS_EMAIL_REPLY_TO}
+        messages = []
+        for user, templates in users.items():
+            if not self.enabled(event, user, paths) or not user.email:
+                continue
+            template = self._get_template(templates)
+            if template is None:
+                continue
+            params = self.prepare(template, instance)
+            e = mail.EmailMultiAlternatives(
+                subject=params['subject'],
+                body=params['description'],
+                from_email=from_address,
+                to=[user.email, ],
+                headers=reply_to
+            )
+            e.attach_alternative(markdown2.markdown(params['description'], extras=["link-patterns"],
+                                                    link_patterns=link_registry.link_patterns(request), safe_mode=True),
+                                 'text/html')
+            messages.append(e)
+        if len(messages):
+            connection = mail.get_connection()
+            connection.send_messages(messages)
+
+    def configured(self, user):
+        return super(EmailMethod, self).configured(user) and user.email is not None
+
+    def _get_configuration(self, user):
+        if not user.email:
+            return {}
+        try:
+            from djembe.models import Identity
+        except ImportError:
+            return {}
+        try:
+            identity = Identity.objects.get(address=user.email)
+        except Identity.DoesNotExist:
+            return {}
+        except Identity.MultipleObjectsReturned:
+            identity = Identity.objects.filter(address=user.email).first()
+        return {'certificate': identity.certificate}
+
+    def form(self, *args, **kwargs):
+        from fir_notifications.forms import EmailMethodConfigurationForm
+        if not len(self.options):
+            return None
+        user = kwargs.pop('user', None)
+        if user is not None:
+            kwargs['initial'] = self._get_configuration(user)
+            kwargs['user'] = user
+        kwargs['method'] = self
+        return EmailMethodConfigurationForm(*args, **kwargs)
diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 56457a0f..d90fbcba 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -1,5 +1,7 @@
 from collections import OrderedDict
 
+from fir_notifications.methods.email import EmailMethod
+
 
 class Notifications(object):
     def __init__(self):
@@ -34,4 +36,5 @@ def get_methods(self):
         return self.methods.values()
 
 
-registry = Notifications()
\ No newline at end of file
+registry = Notifications()
+registry.register_method(EmailMethod())

From 387e6f72c3334919895c0281d1c2bf9f1fa57a21 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 14:25:21 +0100
Subject: [PATCH 15/66] Notifications: add user preference in admin if DEBUG

---
 fir_notifications/admin.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/admin.py b/fir_notifications/admin.py
index 76a22461..5749e746 100644
--- a/fir_notifications/admin.py
+++ b/fir_notifications/admin.py
@@ -3,7 +3,7 @@
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
 
 from fir_plugins.admin import MarkdownModelAdmin
-from fir_notifications.models import MethodConfiguration, NotificationTemplate
+from fir_notifications.models import MethodConfiguration, NotificationTemplate, NotificationPreference
 from fir_notifications.forms import NotificationTemplateForm
 
 
@@ -22,4 +22,5 @@ def business_lines_list(self, obj):
 
 admin.site.register(NotificationTemplate, NotificationTemplateAdmin)
 if settings.DEBUG:
+    admin.site.register(NotificationPreference)
     admin.site.register(MethodConfiguration)

From ad107cd00abe0788a5f4a49a33a7cd748a37b108 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 15:45:18 +0100
Subject: [PATCH 16/66] Notifications: fix super class init call in email
 method

---
 fir_notifications/methods/email.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index bc80bafe..3c0eb734 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -16,7 +16,7 @@ class EmailMethod(NotificationMethod):
     verbose_name = 'Email'
 
     def __init__(self):
-        super(NotificationMethod, self).__init__()
+        super(EmailMethod, self).__init__()
         if hasattr(settings, 'NOTIFICATIONS_EMAIL_FROM'):
             self.server_configured = True
         if 'djembe' in settings.INSTALLED_APPS:

From a5f2523e91eb13958bd5e991abf3e522a77573b7 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 21:20:46 +0100
Subject: [PATCH 17/66] Notifications: add default setting EXTERNAL_URL

---
 fir/config/base.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fir/config/base.py b/fir/config/base.py
index 9f352824..dbe3f0d9 100755
--- a/fir/config/base.py
+++ b/fir/config/base.py
@@ -155,3 +155,6 @@
     # User can change his password
     'CHANGE_PASSWORD': True
 }
+
+# External URL of your FIR application (used in fir_notification to render full URIs in templates)
+EXTERNAL_URL = 'http://fir.example.com'

From f03a98e8ba699e4d1243e502dba8eef421697968 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 21:22:39 +0100
Subject: [PATCH 18/66] Notifications: improve event registry

---
 fir_notifications/registry.py | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index d90fbcba..79697c98 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -1,8 +1,26 @@
 from collections import OrderedDict
 
+from django.apps import apps
+from django.utils.encoding import python_2_unicode_compatible
+
 from fir_notifications.methods.email import EmailMethod
 
 
+@python_2_unicode_compatible
+class RegisteredEvent(object):
+    def __init__(self, name, model, verbose_name=None, section = None):
+        self.name = name
+        if section is None:
+            section = apps.get_app_config(model._meta.app_label).verbose_name
+        self.section = section
+        if verbose_name is None:
+            verbose_name = name
+        self.verbose_name = verbose_name
+
+    def __str__(self):
+        return self.verbose_name
+
+
 class Notifications(object):
     def __init__(self):
         self.methods = OrderedDict()
@@ -19,15 +37,15 @@ def register_method(self, method, name=None, verbose_name=None):
             method.verbose_name = method.name
         self.methods[method.name] = method
 
-    def register_event(self, name, signal, model, callback, verbose_name=None):
+    def register_event(self, name, signal, model, callback, verbose_name=None, section=None):
         if verbose_name is None:
             verbose_name = name
-        self.events[name] = verbose_name
+        self.events[name] = RegisteredEvent(name, model, verbose_name=verbose_name, section=section)
 
         signal.connect(callback, sender=model, dispatch_uid="fir_notifications.{}".format(name))
 
     def get_event_choices(self):
-        return self.events.items()
+        return [(obj.name, obj.verbose_name) for obj in self.events.values()]
 
     def get_method_choices(self):
         return [(obj.name, obj.verbose_name) for obj in self.methods.values()]

From ac1178b7e5df9f66a71c262190b109e7ef509ee1 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 21:24:00 +0100
Subject: [PATCH 19/66] Notifications: add notification user preferences view

---
 fir_notifications/forms.py                    | 75 +++++++++++++++++++
 .../plugins/user_profile.html                 |  3 +
 .../fir_notifications/preferences.html        | 39 ++++++++++
 fir_notifications/urls.py                     |  1 +
 fir_notifications/views.py                    | 37 ++++++++-
 5 files changed, 153 insertions(+), 2 deletions(-)
 create mode 100644 fir_notifications/templates/fir_notifications/plugins/user_profile.html
 create mode 100644 fir_notifications/templates/fir_notifications/preferences.html

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 30743c43..b46195b1 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -1,4 +1,5 @@
 import json
+from collections import OrderedDict
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
@@ -42,3 +43,77 @@ def save(self, *args, **kwargs):
             return None
         config, created = Identity.objects.update_or_create(address=self.user.email, defaults=self.cleaned_data)
         return config
+
+
+class NotificationPreferenceFormset(forms.BaseInlineFormSet):
+    def __init__(self, *args, **kwargs):
+        self.notifications = OrderedDict()
+        for e, verbose_e in registry.events.items():
+            for m, verbose_m in registry.methods.items():
+                self.notifications["{}_{}".format(e, m)] = {'event': e,
+                                                            'verbose_event': verbose_e,
+                                                            'method': m,
+                                                            'verbose_method': verbose_m.verbose_name}
+        self.min_num = len(self.notifications)
+        self.max_num = len(self.notifications)
+        self.can_delete = False
+        super(NotificationPreferenceFormset, self).__init__(*args, **kwargs)
+
+    def _construct_form(self, i, **kwargs):
+        method = None
+        event = None
+        if self.is_bound and i < self.initial_form_count():
+            pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
+            pk = self.data[pk_key]
+            pk_field = self.model._meta.pk
+            to_python = self._get_to_python(pk_field)
+            pk = to_python(pk)
+            instance = self._existing_object(pk)
+            notification = self.notifications.pop("{}_{}".format(instance.event, instance.method))
+            event = notification['verbose_event']
+            method = notification['verbose_method']
+            kwargs['instance'] = instance
+        if i < self.initial_form_count() and 'instance' not in kwargs:
+            instance = self.get_queryset()[i]
+            notification = self.notifications.pop("{}_{}".format(instance.event, instance.method))
+            event = notification['verbose_event']
+            method = notification['verbose_method']
+            kwargs['instance'] = self.get_queryset()[i]
+        if i >= self.initial_form_count() and self.notifications:
+            # Set initial values for extra forms
+            try:
+                key, initial = self.notifications.popitem()
+                event = initial['verbose_event']
+                method = initial['method']
+                kwargs['initial'] = {'event': initial['event'], 'method': initial['method']}
+            except IndexError:
+                pass
+        form = forms.BaseFormSet._construct_form(self, i, **kwargs)
+        if self.save_as_new:
+            # Remove the primary key from the form's data, we are only
+            # creating new instances
+            form.data[form.add_prefix(self._pk_field.name)] = None
+
+            # Remove the foreign key from the form's data
+            form.data[form.add_prefix(self.fk.name)] = None
+
+            # Set the fk value here so that the form can do its validation.
+        fk_value = self.instance.pk
+        if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name:
+            fk_value = getattr(self.instance, self.fk.remote_field.field_name)
+            fk_value = getattr(fk_value, 'pk', fk_value)
+        setattr(form.instance, self.fk.get_attname(), fk_value)
+        setattr(form, 'get_notification_display', lambda: u"{} via {}".format(event.verbose_name, method))
+        setattr(form, 'get_event', lambda: event)
+        return form
+
+    @property
+    def labelled_forms(self):
+        fs_forms = {}
+        for form in self.forms:
+            label = form.get_event().section
+            if label not in fs_forms:
+                fs_forms[label] = []
+            fs_forms[label].append(form)
+            fs_forms[label] = sorted(fs_forms[label], key=lambda form: form.get_event().name)
+        return fs_forms
diff --git a/fir_notifications/templates/fir_notifications/plugins/user_profile.html b/fir_notifications/templates/fir_notifications/plugins/user_profile.html
new file mode 100644
index 00000000..5a810fa2
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/plugins/user_profile.html
@@ -0,0 +1,3 @@
+<div class='fir-async row' id='notifications-preferences' data-fetch-url="{% url 'notifications:preferences' %}">
+
+</div>
\ No newline at end of file
diff --git a/fir_notifications/templates/fir_notifications/preferences.html b/fir_notifications/templates/fir_notifications/preferences.html
new file mode 100644
index 00000000..cae26661
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/preferences.html
@@ -0,0 +1,39 @@
+{% load i18n %}
+{% load add_css_class %}
+
+{% if formset.forms %}
+<div class="col-sm-8 col-sm-offset-2">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">{% trans "Notification preferences" %}</h3>
+      </div>
+      <div class="panel-body">
+          <form action="{% url 'notifications:preferences' %}" method="post">
+                <div class="row-fluid">
+                    {% csrf_token %}
+                    {{ formset.management_form }}
+                    {% for label, forms in formset.labelled_forms.items %}
+                    <fieldset>
+                        <legend>{{ label }}</legend>
+                        {% for form in forms %}
+                        <div class="col-sm-3">
+                            <div class="form-group">
+                           <label id="id_business_lines">{{ form.get_notification_display }}</label>
+                                {{ form.business_lines|add_css_class:"form-control" }}
+                                {% for hidden in form.hidden_fields %}
+                                    {{ hidden }}
+                                {% endfor %}
+                            </div>
+                        </div>
+                        {% endfor %}
+                    </fieldset>
+                    {% endfor %}
+                </div>
+                <div class="row-fluid">
+                    <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
+                </div>
+            </form>
+        </div>
+      </div>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/fir_notifications/urls.py b/fir_notifications/urls.py
index dbb8a939..f7d8dff9 100644
--- a/fir_notifications/urls.py
+++ b/fir_notifications/urls.py
@@ -4,5 +4,6 @@
 
 
 urlpatterns = [
+    url(r'^preferences$', views.preferences, name='preferences'),
     url(r'^preferences/(?P<method>[a-zA-Z0-9_]+)$', views.method_configuration, name='method_configuration'),
 ]
\ No newline at end of file
diff --git a/fir_notifications/views.py b/fir_notifications/views.py
index e34e44e7..100cedd9 100644
--- a/fir_notifications/views.py
+++ b/fir_notifications/views.py
@@ -1,9 +1,15 @@
 from django.contrib.auth.decorators import login_required
-from django.shortcuts import redirect
+from django import forms
+from django.shortcuts import redirect, render
 from django.views.decorators.http import require_POST
+from django.contrib.auth import get_user_model
 
+from fir_notifications.forms import NotificationPreferenceFormset
+from fir_notifications.models import NotificationPreference
 from fir_notifications.registry import registry
 
+from incidents.models import BusinessLine
+
 
 @require_POST
 @login_required
@@ -14,4 +20,31 @@ def method_configuration(request, method):
     form = method_object.form(request.POST, user=request.user)
     if form.is_valid():
         form.save()
-    return redirect('user:profile')
\ No newline at end of file
+    return redirect('user:profile')
+
+
+@login_required
+def preferences(request):
+
+    class NotificationPreferenceForm(forms.ModelForm):
+        event = forms.ChoiceField(choices=registry.get_event_choices(), disabled=True, widget=forms.HiddenInput())
+        method = forms.ChoiceField(choices=registry.get_method_choices(), disabled=True, widget=forms.HiddenInput())
+        business_lines = forms.ModelMultipleChoiceField(BusinessLine.authorization.for_user(request.user,
+                                                                                            'incidents.view_incidents'),
+                                                        required=False)
+
+        class Meta:
+            fields = "__all__"
+
+    formset = forms.inlineformset_factory(get_user_model(), NotificationPreference,
+                                          formset=NotificationPreferenceFormset,
+                                          form=NotificationPreferenceForm)
+    if request.method == 'POST':
+        fs = formset(request.POST, instance=request.user)
+        if fs.is_valid():
+            fs.save()
+        return redirect('user:profile')
+    else:
+        fs = formset(instance=request.user)
+
+    return render(request, "fir_notifications/preferences.html", {'formset': fs})

From 5ad72b22d4cfbac8cb42567a134bf4bb4a8954f8 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 21:38:34 +0100
Subject: [PATCH 20/66] Notifications: add event creation decorator

---
 fir_notifications/decorators.py | 39 +++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 fir_notifications/decorators.py

diff --git a/fir_notifications/decorators.py b/fir_notifications/decorators.py
new file mode 100644
index 00000000..ece33cff
--- /dev/null
+++ b/fir_notifications/decorators.py
@@ -0,0 +1,39 @@
+from django.contrib.contenttypes.models import ContentType
+
+from fir_notifications.registry import registry
+from fir_notifications.tasks import handle_notification
+
+from incidents.models import BusinessLine
+
+
+def notification_event(event, signal, model, verbose_name=None, section=None):
+    """
+    Decorates a Django signal handler to create a notification event
+    Args:
+        event: event name
+        signal: Django signal to listen to
+        model: Django model sending the signal (and event)
+        verbose_name: verbose name of the notification event
+        section: section in the user preference panel (default model application name)
+
+    The signal handler function must return a tuple (model instance, business lines list concerned by the event)
+
+    """
+    def decorator_func(func):
+        def wrapper_func(*args, **kwargs):
+            instance, business_lines = func(*args, **kwargs)
+            if instance is None:
+                return instance, business_lines
+            if isinstance(business_lines, BusinessLine):
+                business_lines = [business_lines.path,]
+            else:
+                business_lines = list(business_lines.distinct().values_list('path', flat=True))
+            handle_notification.delay(ContentType.objects.get_for_model(instance).pk,
+                                      instance.pk,
+                                       business_lines,
+                                      event)
+            return instance, business_lines
+
+        registry.register_event(event, signal, model, wrapper_func, verbose_name, section)
+        return wrapper_func
+    return decorator_func
\ No newline at end of file

From 66f49a719daefdf030ed9e938bbc6b3e69173b7d Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 21:40:08 +0100
Subject: [PATCH 21/66] Notifications: create event and incident related
 notification events

---
 fir_notifications/models.py | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index 816a7081..813ed64b 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -4,8 +4,10 @@
 from django.conf import settings
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
+from fir_notifications.decorators import notification_event
 
 from fir_notifications.registry import registry
+from incidents.models import model_created, Incident, model_updated
 
 
 @python_2_unicode_compatible
@@ -55,3 +57,35 @@ class Meta:
         verbose_name_plural = _('notification preferences')
         unique_together = (("user", "event", "method"),)
         index_together = ["user", "event", "method"]
+
+
+@notification_event('event:created', model_created, Incident, verbose_name=_('Event created'),
+                    section=_('Event'))
+def event_created(sender, instance, **kwargs):
+    if instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines
+
+
+@notification_event('incident:created', model_created, Incident, verbose_name=_('Incident created'),
+                    section=_('Incident'))
+def incident_created(sender, instance, **kwargs):
+    if not instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines
+
+
+@notification_event('event:updated', model_updated, Incident, verbose_name=_('Event updated'),
+                    section=_('Event'))
+def event_created(sender, instance, **kwargs):
+    if instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines
+
+
+@notification_event('incident:updated', model_updated, Incident, verbose_name=_('Incident updated'),
+                    section=_('Incident'))
+def incident_created(sender, instance, **kwargs):
+    if not instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines

From 53e16eefb055082b4a7301df8f189aff8a63d6ab Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 22:11:13 +0100
Subject: [PATCH 22/66] Notifications: move fake request to methods.utils

---
 fir_notifications/methods/__init__.py | 13 -------------
 fir_notifications/methods/email.py    |  3 ++-
 fir_notifications/methods/utils.py    | 13 +++++++++++++
 3 files changed, 15 insertions(+), 14 deletions(-)
 create mode 100644 fir_notifications/methods/utils.py

diff --git a/fir_notifications/methods/__init__.py b/fir_notifications/methods/__init__.py
index 48a13829..0cedde9b 100644
--- a/fir_notifications/methods/__init__.py
+++ b/fir_notifications/methods/__init__.py
@@ -1,19 +1,6 @@
 import json
 
 from django.template import Template, Context
-from django.conf import settings
-
-
-class FakeRequest(object):
-    def __init__(self):
-        self.base = settings.EXTERNAL_URL
-        if self.base.endswith('/'):
-            self.base = self.base[:-1]
-
-    def build_absolute_uri(self, location):
-        return "{}{}".format(self.base, location)
-
-request = FakeRequest()
 
 
 class NotificationMethod(object):
diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index 3c0eb734..8e33ae4b 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -5,7 +5,8 @@
 from django.utils.translation import ugettext_lazy as _
 
 
-from fir_notifications.methods import NotificationMethod, request
+from fir_notifications.methods import NotificationMethod
+from fir_notifications.methods.utils import request
 from fir_plugins.links import registry as link_registry
 
 
diff --git a/fir_notifications/methods/utils.py b/fir_notifications/methods/utils.py
new file mode 100644
index 00000000..434414ff
--- /dev/null
+++ b/fir_notifications/methods/utils.py
@@ -0,0 +1,13 @@
+from django.conf import settings
+
+
+class FakeRequest(object):
+    def __init__(self):
+        self.base = settings.EXTERNAL_URL
+        if self.base.endswith('/'):
+            self.base = self.base[:-1]
+
+    def build_absolute_uri(self, location):
+        return "{}{}".format(self.base, location)
+
+request = FakeRequest()
\ No newline at end of file

From f2781a8816d4ace1de05986d63dc6c47e3b4de8f Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 22:21:34 +0100
Subject: [PATCH 23/66] Notifications: add XMPP method

---
 fir_notifications/methods/jabber.py | 88 +++++++++++++++++++++++++++++
 fir_notifications/registry.py       |  2 +
 fir_notifications/requirements.txt  |  1 +
 3 files changed, 91 insertions(+)
 create mode 100644 fir_notifications/methods/jabber.py
 create mode 100644 fir_notifications/requirements.txt

diff --git a/fir_notifications/methods/jabber.py b/fir_notifications/methods/jabber.py
new file mode 100644
index 00000000..d5b9abf2
--- /dev/null
+++ b/fir_notifications/methods/jabber.py
@@ -0,0 +1,88 @@
+import markdown2
+from django.conf import settings
+
+import xmpppy as xmpp
+from django import forms
+
+from fir_notifications.methods import NotificationMethod
+from fir_notifications.methods.utils import request
+from fir_plugins.links import registry as link_registry
+from django.utils.translation import ugettext_lazy as _
+
+
+class Client(xmpp.Client):
+    def __init__(self, *args, **kwargs):
+        kwargs['debug'] = []
+        xmpp.Client.__init__(self, *args, **kwargs)
+
+    def DisconnectHandler(self):
+        pass
+
+
+class XmppMethod(NotificationMethod):
+    use_subject = True
+    use_short_description = True
+    name = 'xmpp'
+    verbose_name = 'XMPP'
+    options = {
+        'jid': forms.CharField(max_length=100, label=_('Jabber ID'))
+    }
+
+    def __init__(self):
+        super(NotificationMethod, self).__init__()
+        self.messages = []
+        self.jid = getattr(settings, 'NOTIFICATIONS_XMPP_JID', None)
+        self.password = getattr(settings, 'NOTIFICATIONS_XMPP_PASSWORD', None)
+        if self.jid is None or self.password is None:
+            self.server_configured = False
+            return
+        self.server = getattr(settings, 'NOTIFICATIONS_XMPP_SERVER', None)
+        self.port = getattr(settings, 'NOTIFICATIONS_XMPP_SERVER_PORT', 5222)
+        self.connection_tuple = None
+        self.use_srv = True
+        self.jid = xmpp.JID(self.jid)
+        if self.server is not None:
+            self.connection_tuple = (self.server, self.port)
+            self.use_srv = False
+        self.client = Client(self.jid.getDomain())
+        if not self.client.connect(server=self.connection_tuple, use_srv=self.use_srv):
+            self.server_configured = False
+            return
+        if not self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource()):
+            self.server_configured = False
+            return
+        self.client.disconnected()
+        self.server_configured = True
+
+    def send(self, event, users, instance, paths):
+        self.client.reconnectAndReauth()
+        for user, templates in users.items():
+            jid = self._get_jid(user)
+            if not self.enabled(event, user, paths) or jid is None:
+                continue
+            template = self._get_template(templates)
+            if template is None:
+                continue
+            params = self.prepare(template, instance)
+            message = xmpp.protocol.Message(jid, body=params['short_description'].encode('utf-8'),
+                                            subject=params['subject'].encode('utf-8'), typ='chat')
+            html = xmpp.Node('html', {'xmlns': 'http://jabber.org/protocol/xhtml-im'})
+            text = u"<body xmlns='http://www.w3.org/1999/xhtml'>" + markdown2.markdown(params['short_description'],
+                                                                                       extras=["link-patterns"],
+                                                                                       link_patterns=link_registry.link_patterns(
+                                                                                           request),
+                                                                                       safe_mode=True) + u"</body>"
+            html.addChild(node=xmpp.simplexml.XML2Node(text.encode('utf-8')))
+            message.addChild(node=html)
+
+            self.client.send(message)
+        self.client.disconnected()
+
+    def _get_jid(self, user):
+        config = self._get_configuration(user)
+        if 'jid' in config:
+            return xmpp.JID(config['jid'])
+        return None
+
+    def configured(self, user):
+        return super(XmppMethod, self).configured(user) and self._get_jid(user) is not None
diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 79697c98..4de7a5aa 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -4,6 +4,7 @@
 from django.utils.encoding import python_2_unicode_compatible
 
 from fir_notifications.methods.email import EmailMethod
+from fir_notifications.methods.jabber import XmppMethod
 
 
 @python_2_unicode_compatible
@@ -56,3 +57,4 @@ def get_methods(self):
 
 registry = Notifications()
 registry.register_method(EmailMethod())
+registry.register_method(XmppMethod())
diff --git a/fir_notifications/requirements.txt b/fir_notifications/requirements.txt
new file mode 100644
index 00000000..78bd362e
--- /dev/null
+++ b/fir_notifications/requirements.txt
@@ -0,0 +1 @@
+-e git+https://github.com/ArchipelProject/xmpppy.git@288b280c6ec534c100bfee871daa3bb707467a1a#egg=xmpppy
\ No newline at end of file

From d4ac392b7edcb02ecc7e7ee31efa3307a122b82c Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 22:26:00 +0100
Subject: [PATCH 24/66] Notifications: add Readme

---
 fir_notifications/README.md | 106 ++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)
 create mode 100644 fir_notifications/README.md

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
new file mode 100644
index 00000000..c8cf9000
--- /dev/null
+++ b/fir_notifications/README.md
@@ -0,0 +1,106 @@
+# Notifications plugin for FIR
+
+## Features
+
+This plugins allows you to launch asynchronous tasks with Celery and send notifications to users.
+
+## Installation
+
+In your FIR virtualenv, launch:
+
+```bash
+(fir_env)$ pip install -r fir_notifications/requirements.txt
+```
+
+In *$FIR_HOME/fir/config/installed_app.txt*, add:
+
+```
+fir_notifications
+```
+
+In your *$FIR_HOME*, launch:
+
+```bash
+(your_env)$ ./manage.py migrate fir_notifications
+(your_env)$ ./manage.py collectstatic -y
+```
+
+You should configure fir_celery (broker and result backend).
+
+## Usage
+
+Users can subscribe to notifications via their profile page.
+
+Core FIR notifications:
+* 'event:created': new event
+* 'event:updated': update of an event
+* 'incident:created': new incident
+* 'incident:updated': update of an incident
+
+## Configuration
+
+### Celery
+
+`fir_notifications` uses the FIR plugin `fir_celery`.
+
+### Full URL in notification links
+
+To generate correct URL in notification, `fir_notifications` needs to know the external URL of the FIR site:
+
+``` python
+EXTERNAL_URL = 'https://fir.example.com'
+```
+
+### Email notifications
+
+You have to configure [Django email backend](https://docs.djangoproject.com/en/1.9/topics/email/).
+
+In addition, `fir_notifications` uses two settings:
+
+``` python
+# From address (required)
+NOTIFICATIONS_EMAIL_FROM = 'fir@example.com'
+# Reply to address (optional)
+NOTIFICATIONS_EMAIL_REPLY_TO = None
+```
+
+To send signed/encrypted email notifications with S/MIME to users, install and configure [django-djembe](https://github.com/cabincode/django-djembe) and add it in your *installed_apps.txt*.
+
+### Jabber (XMPP) notifications
+
+Configure `fir_notifications`:
+
+``` python
+# FIR user JID 
+NOTIFICATIONS_XMPP_JID = 'fir@example.com'
+# Password for fir@example.com JID
+NOTIFICATIONS_XMPP_PASSWORD = 'my secret password'
+# XMPP server
+NOTIFICATIONS_XMPP_SERVER = 'localhost'
+# XMPP server port
+NOTIFICATIONS_XMPP_PORT = 5222
+```
+
+### Notification templates
+
+You have to create notification templates in the Django admin site.
+
+To render notifications, each notification method can use the fields `subject`, `description` or `short_description`:
+
+- Email uses `subject` and `description`.
+- XMPP uses `subject` and `short_description`.
+
+## Hacking
+
+### Adding notification method
+
+You have to create a subclass of `NotificationMethod` from `fir_notifications.methods` and implement at least the `send` method. You can then register your method with `fir_notification.registry.registry.register_method`.
+
+If your configuration method needs some additional user defined settings, you have to list them in the class property `options`. See `EmailMethod` and `XmppMethod` for details. 
+
+### Adding notification event
+
+Use the `@notification_event` decorator defined in `fir_notifications.decorators` to decorate a classic Django signal handler function. This handler must return a tuple with an instance of the notification model and a queryset of the concerned business lines.
+
+
+

From 05683de223e55d2d2810ec8c4faeaf5befe3b535 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 22:37:55 +0100
Subject: [PATCH 25/66] Notifications: add certificate form field label

---
 fir_notifications/methods/email.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index 8e33ae4b..377d7798 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -21,7 +21,7 @@ def __init__(self):
         if hasattr(settings, 'NOTIFICATIONS_EMAIL_FROM'):
             self.server_configured = True
         if 'djembe' in settings.INSTALLED_APPS:
-            self.options['certificate'] = forms.CharField(required=False,
+            self.options['certificate'] = forms.CharField(required=False, label=_('Certificate'),
                                                           widget=forms.Textarea(attrs={'cols': 60, 'rows': 15}),
                                                           help_text=_('Encryption certificate in PEM format.'))
 

From 381455f88122f8cb5e15b860e1b118ae5247d08f Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sat, 14 Jan 2017 22:38:19 +0100
Subject: [PATCH 26/66] Notifications: add French translation

---
 .../locale/fr/LC_MESSAGES/django.po           | 151 ++++++++++++++++++
 1 file changed, 151 insertions(+)
 create mode 100644 fir_notifications/locale/fr/LC_MESSAGES/django.po

diff --git a/fir_notifications/locale/fr/LC_MESSAGES/django.po b/fir_notifications/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..dc8f146e
--- /dev/null
+++ b/fir_notifications/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,151 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-01-14 22:33+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: admin.py:19
+msgctxt "business lines"
+msgid "All"
+msgstr "Toutes"
+
+#: admin.py:20
+msgid "Business lines"
+msgstr ""
+
+#: apps.py:7
+msgid "Notifications"
+msgstr ""
+
+#: forms.py:18
+#, python-format
+msgid "Configure %(method)s"
+msgstr "Configurer %(method)s"
+
+#: methods/email.py:24
+msgid "Certificate"
+msgstr "Certificat"
+
+#: methods/email.py:26
+msgid "Encryption certificate in PEM format."
+msgstr "Certificat de chiffrement au format PEM"
+
+#: methods/jabber.py:28
+msgid "Jabber ID"
+msgstr ""
+
+#: models.py:15 models.py:44
+msgid "user"
+msgstr "utilisateur"
+
+#: models.py:16 models.py:46
+msgid "method"
+msgstr "méthode"
+
+#: models.py:17
+msgid "configuration"
+msgstr ""
+
+#: models.py:23
+msgid "method configuration"
+msgstr "configuration de méthode"
+
+#: models.py:24
+msgid "method configurations"
+msgstr "configurations de méthode"
+
+#: models.py:30 models.py:45
+msgid "event"
+msgstr "événement"
+
+#: models.py:32
+msgid "business line"
+msgstr ""
+
+#: models.py:33
+msgid "subject"
+msgstr "objet"
+
+#: models.py:34
+msgid "short description"
+msgstr "description courte"
+
+#: models.py:35
+msgid "description"
+msgstr ""
+
+#: models.py:38
+msgid "notification template"
+msgstr "gabarit de notification"
+
+#: models.py:39
+msgid "notification templates"
+msgstr "gabarits de notification"
+
+#: models.py:48
+msgid "business lines"
+msgstr ""
+
+#: models.py:56
+msgid "notification preference"
+msgstr "préférence de notification"
+
+#: models.py:57
+msgid "notification preferences"
+msgstr "préférences de notification"
+
+#: models.py:62
+msgid "Event created"
+msgstr "Événement créé"
+
+#: models.py:63 models.py:79
+msgid "Event"
+msgstr "Événement"
+
+#: models.py:70
+msgid "Incident created"
+msgstr "Incident créé"
+
+#: models.py:71 models.py:87
+msgid "Incident"
+msgstr "Incident"
+
+#: models.py:78
+msgid "Event updated"
+msgstr "Événement mis à jour"
+
+#: models.py:86
+msgid "Incident updated"
+msgstr "Incident mis à jour"
+
+#: templates/fir_notifications/actions.html:3
+#, python-format
+msgid "Configure %(method_name)s"
+msgstr "Configurer %(method_name)s"
+
+#: templates/fir_notifications/actions_form.html:38
+msgid "Cancel"
+msgstr "Annuler"
+
+#: templates/fir_notifications/actions_form.html:39
+#: templates/fir_notifications/preferences.html:33
+msgid "Save"
+msgstr "Enregistrer"
+
+#: templates/fir_notifications/preferences.html:8
+msgid "Notification preferences"
+msgstr "Préférences de notification"

From 71e64bd4994a986eff57a2f4a83acd7ce33d3710 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Sun, 15 Jan 2017 11:47:22 +0100
Subject: [PATCH 27/66] Notifications: fix method display in user preferences

---
 fir_notifications/forms.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index b46195b1..8fce4d57 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -84,7 +84,7 @@ def _construct_form(self, i, **kwargs):
             try:
                 key, initial = self.notifications.popitem()
                 event = initial['verbose_event']
-                method = initial['method']
+                method = initial['verbose_method']
                 kwargs['initial'] = {'event': initial['event'], 'method': initial['method']}
             except IndexError:
                 pass

From 89c250e2973d0a6ddb05e862f56bb7de370e14f6 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 07:48:23 +0100
Subject: [PATCH 28/66] Notifications: update Readme: How to send
 encrypted/signed email notifications (S/MIME)

---
 fir_notifications/README.md              | 48 ++++++++++++++++++++++--
 fir_notifications/requirements_smime.txt |  1 +
 2 files changed, 46 insertions(+), 3 deletions(-)
 create mode 100644 fir_notifications/requirements_smime.txt

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index c8cf9000..5f8e0205 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -9,7 +9,7 @@ This plugins allows you to launch asynchronous tasks with Celery and send notifi
 In your FIR virtualenv, launch:
 
 ```bash
-(fir_env)$ pip install -r fir_notifications/requirements.txt
+(fir-env)$ pip install -r fir_notifications/requirements.txt
 ```
 
 In *$FIR_HOME/fir/config/installed_app.txt*, add:
@@ -21,8 +21,7 @@ fir_notifications
 In your *$FIR_HOME*, launch:
 
 ```bash
-(your_env)$ ./manage.py migrate fir_notifications
-(your_env)$ ./manage.py collectstatic -y
+(fir-env)$ ./manage.py migrate fir_notifications
 ```
 
 You should configure fir_celery (broker and result backend).
@@ -64,8 +63,51 @@ NOTIFICATIONS_EMAIL_FROM = 'fir@example.com'
 NOTIFICATIONS_EMAIL_REPLY_TO = None
 ```
 
+### S/MIME
+
 To send signed/encrypted email notifications with S/MIME to users, install and configure [django-djembe](https://github.com/cabincode/django-djembe) and add it in your *installed_apps.txt*.
 
+The following configuration example from the Djembe Readme can help you:
+
+``` bash
+(fir-env)$ pip install -r fir_notifications/requirements_smime.txt
+(fir-env)$ python manage.py migrate djembe
+```
+
+In *$FIR_HOME/fir/config/installed_app.txt*, add:
+
+```
+djembe
+```
+
+Change your email backend in your settings:
+
+``` python
+EMAIL_BACKEND = 'djembe.backends.EncryptingSMTPBackend'
+```
+
+To use a cipher other than the default AES-256, specify it in your settings `DJEMBE_CIPHER`:
+
+
+``` python
+DJEMBE_CIPHER = 'des_ede3_cbc'  # triple DES
+```
+The intersection of M2Crypto's ciphers and RFC 3851 are:
+
+* `des_ede3_cbc` (required by the RFC)
+* `aes_128_cbc` (recommended, not required, by the RFC)
+* `aes_192_cbc` (recommended, not required, by the RFC)
+* `aes_256_cbc` (recommended, not required, by the RFC)
+* `rc2_40_cbc` (RFC requires support, but it's weak -- don't use it)
+
+RFC 5751 requires AES-128, and indicates that higher key lengths are of
+course the future. It marks tripleDES with "SHOULD-", meaning it's on its
+way out.
+
+To create signed notifications, in the admin site (*Djembe > Identities*), supply both a certificate and a private key which must not have a passphrase, with an `Address` that is the same as your setting `NOTIFICATIONS_EMAIL_FROM`. Any mail sent *from* this Identity's address will be signed with the private key.
+
+User certificates will be added from the user profile in FIR (*Configure Email*).
+
 ### Jabber (XMPP) notifications
 
 Configure `fir_notifications`:
diff --git a/fir_notifications/requirements_smime.txt b/fir_notifications/requirements_smime.txt
new file mode 100644
index 00000000..e1abc67f
--- /dev/null
+++ b/fir_notifications/requirements_smime.txt
@@ -0,0 +1 @@
+django-djembe==0.2.0
\ No newline at end of file

From 897e2f1feb87aa8e748928126f7347f39b7c69b1 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 08:00:15 +0100
Subject: [PATCH 29/66] Notifications: Readme, more on templates and template
 selection

---
 fir_notifications/README.md | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 5f8e0205..25e8e6af 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -63,7 +63,7 @@ NOTIFICATIONS_EMAIL_FROM = 'fir@example.com'
 NOTIFICATIONS_EMAIL_REPLY_TO = None
 ```
 
-### S/MIME
+#### S/MIME
 
 To send signed/encrypted email notifications with S/MIME to users, install and configure [django-djembe](https://github.com/cabincode/django-djembe) and add it in your *installed_apps.txt*.
 
@@ -125,13 +125,20 @@ NOTIFICATIONS_XMPP_PORT = 5222
 
 ### Notification templates
 
-You have to create notification templates in the Django admin site.
+You have to create at least onenotification template per notification event in the Django admin site.
 
 To render notifications, each notification method can use the fields `subject`, `description` or `short_description`:
 
 - Email uses `subject` and `description`.
 - XMPP uses `subject` and `short_description`.
 
+These fields will accept Markdown formatted text and Django template language markups. The Django template context will contain an `instance`object, the instance of the object that fired the notification event.
+
+The Email `description` will generate a multipart message: a plain text part in Markdown and a HTML part rendered from this Markdown. The XMPP `short_description` will be rendered as HTML from Markdown.
+
+The template used to send the notification to the user will be chosen from the templates available to this user:
+- For a user with global permissions (permission from a Django group), global templates (templates with no nusiness line attached to it) will be preferred. 
+- For a user with no global permission, the nearest business line template will be preferred, global templates will be used as a fallback.
 ## Hacking
 
 ### Adding notification method

From 4eecbe367be660c3acb7a98e3b7a25d29c6a87d4 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 08:01:08 +0100
Subject: [PATCH 30/66] Notifications: update Readme

---
 fir_notifications/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 25e8e6af..356ef40c 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -2,7 +2,7 @@
 
 ## Features
 
-This plugins allows you to launch asynchronous tasks with Celery and send notifications to users.
+This plugins allows you to send notifications to users.
 
 ## Installation
 

From 7c46258ff79481392afd1f3c614efb130e04058b Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 08:06:55 +0100
Subject: [PATCH 31/66] Notifications: document the event registry

---
 fir_notifications/registry.py | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 4de7a5aa..9201679e 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -28,6 +28,13 @@ def __init__(self):
         self.events = OrderedDict()
 
     def register_method(self, method, name=None, verbose_name=None):
+        """
+        Registers a notification method, instance of a subclass of `fir_notifications.methods.NotificationMethod`
+        Args:
+            method: instance of the notification method
+            name: overrides the instance.name
+            verbose_name: overrides the instance.verbose_name
+        """
         if not method.server_configured:
             return
         if name is not None:
@@ -39,6 +46,16 @@ def register_method(self, method, name=None, verbose_name=None):
         self.methods[method.name] = method
 
     def register_event(self, name, signal, model, callback, verbose_name=None, section=None):
+        """
+        Registers a notification event
+        Args:
+            name: event name
+            signal: Django signal to listen to
+            model: Django model sending the signal (and event)
+            callback: Django signal handler
+            verbose_name: verbose name of the event
+            section: section in the user preference panel (default model application name)
+        """
         if verbose_name is None:
             verbose_name = name
         self.events[name] = RegisteredEvent(name, model, verbose_name=verbose_name, section=section)

From e4f8b513865bf4fd0507d937994ff70abff3c919 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 10:03:40 +0100
Subject: [PATCH 32/66] Notifications: Fix user preferences error (when a
 method is disabled after preferences where saved for this method)

---
 fir_notifications/forms.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 8fce4d57..b55930cf 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -57,6 +57,13 @@ def __init__(self, *args, **kwargs):
         self.min_num = len(self.notifications)
         self.max_num = len(self.notifications)
         self.can_delete = False
+        instance = kwargs.get('instance', None)
+        if instance is not None:
+            queryset = kwargs.get('queryset', None)
+            if queryset is None:
+                queryset = self.model._default_manager
+            qs = queryset.filter(event__in=registry.events.keys(), method__in= registry.methods.keys())
+            kwargs['queryset'] = qs
         super(NotificationPreferenceFormset, self).__init__(*args, **kwargs)
 
     def _construct_form(self, i, **kwargs):

From c59ec2225f217a92c5774cccbf389000bbb35d33 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 10:06:46 +0100
Subject: [PATCH 33/66] Notifications: use settings defined in fir_email

---
 fir_notifications/README.md        |  8 ++++----
 fir_notifications/methods/email.py | 10 +++++-----
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 356ef40c..e6ad752e 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -54,13 +54,13 @@ EXTERNAL_URL = 'https://fir.example.com'
 
 You have to configure [Django email backend](https://docs.djangoproject.com/en/1.9/topics/email/).
 
-In addition, `fir_notifications` uses two settings:
+In addition, `fir_notifications` uses two settings defined in `fir_email`:
 
 ``` python
 # From address (required)
-NOTIFICATIONS_EMAIL_FROM = 'fir@example.com'
+EMAIL_FROM = 'fir@example.com'
 # Reply to address (optional)
-NOTIFICATIONS_EMAIL_REPLY_TO = None
+REPLY_TO = None
 ```
 
 #### S/MIME
@@ -104,7 +104,7 @@ RFC 5751 requires AES-128, and indicates that higher key lengths are of
 course the future. It marks tripleDES with "SHOULD-", meaning it's on its
 way out.
 
-To create signed notifications, in the admin site (*Djembe > Identities*), supply both a certificate and a private key which must not have a passphrase, with an `Address` that is the same as your setting `NOTIFICATIONS_EMAIL_FROM`. Any mail sent *from* this Identity's address will be signed with the private key.
+To create signed notifications, in the admin site (*Djembe > Identities*), supply both a certificate and a private key which must not have a passphrase, with an `Address` that is the same as your setting `EMAIL_FROM`. Any mail sent *from* this Identity's address will be signed with the private key.
 
 User certificates will be added from the user profile in FIR (*Configure Email*).
 
diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index 377d7798..50e28581 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -18,7 +18,7 @@ class EmailMethod(NotificationMethod):
 
     def __init__(self):
         super(EmailMethod, self).__init__()
-        if hasattr(settings, 'NOTIFICATIONS_EMAIL_FROM'):
+        if hasattr(settings, 'EMAIL_FROM') and settings.EMAIL_FROM is not None:
             self.server_configured = True
         if 'djembe' in settings.INSTALLED_APPS:
             self.options['certificate'] = forms.CharField(required=False, label=_('Certificate'),
@@ -26,11 +26,11 @@ def __init__(self):
                                                           help_text=_('Encryption certificate in PEM format.'))
 
     def send(self, event, users, instance, paths):
-        from_address = settings.NOTIFICATIONS_EMAIL_FROM
+        from_address = settings.EMAIL_FROM
         reply_to = {}
-        if hasattr(settings, 'NOTIFICATIONS_EMAIL_REPLY_TO'):
-            reply_to = {'Reply-To': settings.NOTIFICATIONS_EMAIL_REPLY_TO,
-                        'Return-Path': settings.NOTIFICATIONS_EMAIL_REPLY_TO}
+        if hasattr(settings, 'REPLY_TO'):
+            reply_to = {'Reply-To': settings.REPLY_TO,
+                        'Return-Path': settings.REPLY_TO}
         messages = []
         for user, templates in users.items():
             if not self.enabled(event, user, paths) or not user.email:

From 84b6fe1e1afdaea042f86cb67deb46666247a922 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 16 Jan 2017 19:06:02 +0100
Subject: [PATCH 34/66] Notifications: fir_celery integration: remove
 shared_task and use celery_app

---
 fir_notifications/tasks.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/tasks.py b/fir_notifications/tasks.py
index 1b1b300d..f42f652c 100644
--- a/fir_notifications/tasks.py
+++ b/fir_notifications/tasks.py
@@ -5,6 +5,7 @@
 
 from django.contrib.auth.models import User, Permission
 from incidents.models import BusinessLine
+from fir_celery.celeryconf import celery_app
 
 _perm_id = None
 
@@ -83,7 +84,7 @@ def get_user_templates(event, business_lines):
     return users
 
 
-@shared_task
+@celery_app.task
 def handle_notification(content_type, instance, business_lines, event):
     from fir_notifications.registry import registry
     from django.contrib.contenttypes.models import ContentType

From 73fad53b0a6e3dd2722979fd81d20c7365a76d64 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 08:13:21 +0100
Subject: [PATCH 35/66] Notifications: rename duplicate signal handlers

---
 fir_notifications/models.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index 813ed64b..d2d13950 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -77,7 +77,7 @@ def incident_created(sender, instance, **kwargs):
 
 @notification_event('event:updated', model_updated, Incident, verbose_name=_('Event updated'),
                     section=_('Event'))
-def event_created(sender, instance, **kwargs):
+def event_updated(sender, instance, **kwargs):
     if instance.is_incident:
         return None, None
     return instance, instance.concerned_business_lines
@@ -85,7 +85,7 @@ def event_created(sender, instance, **kwargs):
 
 @notification_event('incident:updated', model_updated, Incident, verbose_name=_('Incident updated'),
                     section=_('Incident'))
-def incident_created(sender, instance, **kwargs):
+def incident_updated(sender, instance, **kwargs):
     if not instance.is_incident:
         return None, None
     return instance, instance.concerned_business_lines

From 1d9a9ff26a704398d7d17693e7480610af697c35 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 09:14:55 +0100
Subject: [PATCH 36/66] Notifications: add model_status_changed signal in
 incidents

---
 incidents/models.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/incidents/models.py b/incidents/models.py
index f141f61b..34e18a6b 100755
--- a/incidents/models.py
+++ b/incidents/models.py
@@ -50,6 +50,7 @@
 
 model_created = Signal(providing_args=['instance'])
 model_updated = Signal(providing_args=['instance'])
+model_status_changed = Signal(providing_args=['instance', 'previous_status'])
 
 
 class FIRModel:
@@ -198,8 +199,10 @@ def is_open(self):
         return self.get_last_action != "Closed"
 
     def close_timeout(self):
+        previous_status = self.status
         self.status = 'C'
         self.save()
+        model_status_changed.send(sender=Incident, instance=self, previous_status=previous_status)
 
         c = Comments()
         c.comment = "Incident closed (timeout)"

From 41ce12ef048647f5c3d8219341a0f305c6878e54 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 09:15:27 +0100
Subject: [PATCH 37/66] Notifications: use model_status_changed signal in
 incidents views

---
 incidents/views.py | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/incidents/views.py b/incidents/views.py
index 4c07f2aa..19c9afe3 100755
--- a/incidents/views.py
+++ b/incidents/views.py
@@ -8,7 +8,7 @@
 from django.template.response import TemplateResponse
 from django.views.decorators.http import require_POST
 
-from incidents.models import IncidentCategory, Incident, Comments, BusinessLine
+from incidents.models import IncidentCategory, Incident, Comments, BusinessLine, model_status_changed
 from incidents.models import Label, Log, BaleCategory
 from incidents.models import Attribute, ValidAttribute, IncidentTemplate, Profile
 from incidents.models import IncidentForm, CommentForm
@@ -338,13 +338,17 @@ def edit_incident(request, incident_id, authorization_target=None):
     starred = i.is_starred
 
     if request.method == "POST":
+        previous_status = i.status
         form = IncidentForm(request.POST, instance=i, for_user=request.user)
 
         if form.is_valid():
             Comments.create_diff_comment(i, form.cleaned_data, request.user)
-
+            if previous_status == form.cleaned_data['status']:
+                previous_status = None
             # update main BL
             form.save()
+            if previous_status is not None:
+                model_status_changed.send(sender=Incident, instance=i, previous_status=previous_status)
             i.refresh_main_business_lines()
             i.is_starred = starred
             i.save()
@@ -386,6 +390,7 @@ def change_status(request, incident_id, status, authorization_target=None):
             pk=incident_id)
     else:
         i = authorization_target
+    previous_status = i.status
     i.status = status
     i.save()
 
@@ -402,7 +407,7 @@ def change_status(request, incident_id, status, authorization_target=None):
     c.incident = i
     c.opened_by = request.user
     c.save()
-
+    model_status_changed.send(sender=Incident, instance=i, previous_status=previous_status)
     return redirect('dashboard:main')
 
 
@@ -522,8 +527,11 @@ def update_comment(request, comment_id):
                 incident=c.incident)
 
             if c.action.name in ['Closed', 'Opened', 'Blocked']:
-                c.incident.status = c.action.name[0]
-                c.incident.save()
+                if c.action.name[0] != c.incident.status:
+                    previous_status = c.incident.status
+                    c.incident.status = c.action.name[0]
+                    c.incident.save()
+                    model_status_changed.send(sender=Incident, instance=c.incident, previous_status=previous_status)
 
             i.refresh_artifacts(c.comment)
 
@@ -773,9 +781,11 @@ def comment(request, incident_id, authorization_target=None):
             log("Comment created: %s" % (com.comment[:20] + "..."), request.user, incident=com.incident)
             i.refresh_artifacts(com.comment)
 
-            if com.action.name in ['Closed', 'Opened', 'Blocked']:
+            if com.action.name in ['Closed', 'Opened', 'Blocked'] and com.incident.status != com.action.name[0]:
+                previous_status = com.incident.status
                 com.incident.status = com.action.name[0]
                 com.incident.save()
+                model_status_changed.send(sender=Incident, instance=com.incident, previous_status=previous_status)
 
             return render(request, 'events/_comment.html', {'event': i, 'comment': com})
         else:

From 596e03a90779d66565433f88ac552649f337380e Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 09:35:43 +0100
Subject: [PATCH 38/66] Notifications: event and incident new notification
 events (commented, status changed)

---
 fir_notifications/README.md |  4 ++++
 fir_notifications/models.py | 39 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index e6ad752e..2713516a 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -35,6 +35,10 @@ Core FIR notifications:
 * 'event:updated': update of an event
 * 'incident:created': new incident
 * 'incident:updated': update of an incident
+* 'event:commented': new comment added to an event
+* 'incident:commented': new comment added to an incident
+* 'event:status_changed': event status changed
+* 'incident:status_changed': incident status changed
 
 ## Configuration
 
diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index d2d13950..4b1603e3 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -2,12 +2,13 @@
 
 from django.db import models
 from django.conf import settings
+from django.db.models.signals import post_save
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 from fir_notifications.decorators import notification_event
 
 from fir_notifications.registry import registry
-from incidents.models import model_created, Incident, model_updated
+from incidents.models import model_created, Incident, model_updated, Comments, model_status_changed
 
 
 @python_2_unicode_compatible
@@ -89,3 +90,39 @@ def incident_updated(sender, instance, **kwargs):
     if not instance.is_incident:
         return None, None
     return instance, instance.concerned_business_lines
+
+
+@notification_event('event:commented', post_save, Comments, verbose_name=_('Event commented'),
+                    section=_('Event'))
+def event_commented(sender, instance, **kwargs):
+    if not instance.incident and instance.incident.is_incident:
+        return None, None
+    if instance.action.name in ['Opened', 'Blocked', 'Closed']:
+        return None, None
+    return instance, instance.incident.concerned_business_lines
+
+
+@notification_event('incident:commented', post_save, Comments, verbose_name=_('Incident commented'),
+                    section=_('Incident'))
+def incident_commented(sender, instance, **kwargs):
+    if not instance.incident and not instance.incident.is_incident:
+        return None, None
+    if instance.action.name in ['Opened', 'Blocked', 'Closed']:
+        return None, None
+    return instance, instance.incident.concerned_business_lines
+
+
+@notification_event('event:status_changed', model_status_changed, Incident, verbose_name=_('Event status changed'),
+                    section=_('Event'))
+def event_status_changed(sender, instance, **kwargs):
+    if instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines
+
+
+@notification_event('incident:status_changed', model_status_changed, Incident, verbose_name=_('Incident status changed'),
+                    section=_('Incident'))
+def incident_status_changed(sender, instance, **kwargs):
+    if not instance.is_incident:
+        return None, None
+    return instance, instance.concerned_business_lines
\ No newline at end of file

From 46c8441be47c315eb1583302721daa5b87f6bcb7 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 09:38:26 +0100
Subject: [PATCH 39/66] Notifications: French translation for new notification
 events

---
 .../locale/fr/LC_MESSAGES/django.po           | 64 ++++++++++++-------
 1 file changed, 42 insertions(+), 22 deletions(-)

diff --git a/fir_notifications/locale/fr/LC_MESSAGES/django.po b/fir_notifications/locale/fr/LC_MESSAGES/django.po
index dc8f146e..89980538 100644
--- a/fir_notifications/locale/fr/LC_MESSAGES/django.po
+++ b/fir_notifications/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-01-14 22:33+0100\n"
+"POT-Creation-Date: 2017-01-17 08:48+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -48,90 +48,110 @@ msgstr "Certificat de chiffrement au format PEM"
 msgid "Jabber ID"
 msgstr ""
 
-#: models.py:15 models.py:44
+#: models.py:16 models.py:45
 msgid "user"
 msgstr "utilisateur"
 
-#: models.py:16 models.py:46
+#: models.py:17 models.py:47
 msgid "method"
 msgstr "méthode"
 
-#: models.py:17
+#: models.py:18
 msgid "configuration"
 msgstr ""
 
-#: models.py:23
+#: models.py:24
 msgid "method configuration"
 msgstr "configuration de méthode"
 
-#: models.py:24
+#: models.py:25
 msgid "method configurations"
 msgstr "configurations de méthode"
 
-#: models.py:30 models.py:45
+#: models.py:31 models.py:46
 msgid "event"
 msgstr "événement"
 
-#: models.py:32
+#: models.py:33
 msgid "business line"
 msgstr ""
 
-#: models.py:33
+#: models.py:34
 msgid "subject"
 msgstr "objet"
 
-#: models.py:34
+#: models.py:35
 msgid "short description"
 msgstr "description courte"
 
-#: models.py:35
+#: models.py:36
 msgid "description"
 msgstr ""
 
-#: models.py:38
+#: models.py:39
 msgid "notification template"
 msgstr "gabarit de notification"
 
-#: models.py:39
+#: models.py:40
 msgid "notification templates"
 msgstr "gabarits de notification"
 
-#: models.py:48
+#: models.py:49
 msgid "business lines"
 msgstr ""
 
-#: models.py:56
+#: models.py:57
 msgid "notification preference"
 msgstr "préférence de notification"
 
-#: models.py:57
+#: models.py:58
 msgid "notification preferences"
 msgstr "préférences de notification"
 
-#: models.py:62
+#: models.py:63
 msgid "Event created"
 msgstr "Événement créé"
 
-#: models.py:63 models.py:79
+#: models.py:64 models.py:80 models.py:96 models.py:118
 msgid "Event"
 msgstr "Événement"
 
-#: models.py:70
+#: models.py:71
 msgid "Incident created"
 msgstr "Incident créé"
 
-#: models.py:71 models.py:87
+#: models.py:72 models.py:88 models.py:107 models.py:126
 msgid "Incident"
 msgstr "Incident"
 
-#: models.py:78
+#: models.py:79
 msgid "Event updated"
 msgstr "Événement mis à jour"
 
-#: models.py:86
+#: models.py:87
 msgid "Incident updated"
 msgstr "Incident mis à jour"
 
+#: models.py:95
+#| msgid "Event created"
+msgid "Event commented"
+msgstr "Événement commenté"
+
+#: models.py:106
+#| msgid "Incident created"
+msgid "Incident commented"
+msgstr "Incident commenté"
+
+#: models.py:117
+#| msgid "Event created"
+msgid "Event status changed"
+msgstr "Statut de l'événement changé"
+
+#: models.py:125
+#| msgid "Incident created"
+msgid "Incident status changed"
+msgstr "Statut de l'incident changé"
+
 #: templates/fir_notifications/actions.html:3
 #, python-format
 msgid "Configure %(method_name)s"

From 2bb5172b6bb64e1a20ea78576cb671a3efb7d4e0 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 09:40:40 +0100
Subject: [PATCH 40/66] Notifications: notification events can be disabled in
 setting NOTIFICATIONS_DISABLED_EVENTS

---
 fir/config/base.py            | 4 ++++
 fir_notifications/README.md   | 8 ++++++++
 fir_notifications/registry.py | 3 +++
 3 files changed, 15 insertions(+)

diff --git a/fir/config/base.py b/fir/config/base.py
index dbe3f0d9..4620bd03 100755
--- a/fir/config/base.py
+++ b/fir/config/base.py
@@ -158,3 +158,7 @@
 
 # External URL of your FIR application (used in fir_notification to render full URIs in templates)
 EXTERNAL_URL = 'http://fir.example.com'
+
+# Put notification events you don't want in this tuple
+# Example: NOTIFICATIONS_DISABLED_EVENTS = ('event:created', 'incident:created')
+NOTIFICATIONS_DISABLED_EVENTS = ()
diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 2713516a..721a9f13 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -42,6 +42,14 @@ Core FIR notifications:
 
 ## Configuration
 
+### Events
+
+You can disable notification events in the settings using the key `NOTIFICATIONS_DISABLED_EVENTS`:
+
+```python
+NOTIFICATIONS_DISABLED_EVENTS = ('event:created', 'incident:created')
+```
+
 ### Celery
 
 `fir_notifications` uses the FIR plugin `fir_celery`.
diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 9201679e..77ef9a59 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -2,6 +2,7 @@
 
 from django.apps import apps
 from django.utils.encoding import python_2_unicode_compatible
+from django.conf import settings
 
 from fir_notifications.methods.email import EmailMethod
 from fir_notifications.methods.jabber import XmppMethod
@@ -56,6 +57,8 @@ def register_event(self, name, signal, model, callback, verbose_name=None, secti
             verbose_name: verbose name of the event
             section: section in the user preference panel (default model application name)
         """
+        if name in settings.NOTIFICATIONS_DISABLED_EVENTS:
+            return
         if verbose_name is None:
             verbose_name = name
         self.events[name] = RegisteredEvent(name, model, verbose_name=verbose_name, section=section)

From b26007358afa821400af8acf9e42ab94f827f427 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Tue, 17 Jan 2017 10:04:23 +0100
Subject: [PATCH 41/66] Notifications: 
 NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS setting

---
 fir/config/base.py          |  3 ++
 fir_notifications/README.md |  7 ++++
 fir_notifications/models.py | 71 +++++++++++++++++++------------------
 3 files changed, 46 insertions(+), 35 deletions(-)

diff --git a/fir/config/base.py b/fir/config/base.py
index 4620bd03..b98d9bdd 100755
--- a/fir/config/base.py
+++ b/fir/config/base.py
@@ -162,3 +162,6 @@
 # Put notification events you don't want in this tuple
 # Example: NOTIFICATIONS_DISABLED_EVENTS = ('event:created', 'incident:created')
 NOTIFICATIONS_DISABLED_EVENTS = ()
+
+# Send 'incident:*' notification events for both Event and Incident if True
+NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS = False
diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 721a9f13..51aeb53a 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -50,6 +50,13 @@ You can disable notification events in the settings using the key `NOTIFICATIONS
 NOTIFICATIONS_DISABLED_EVENTS = ('event:created', 'incident:created')
 ```
 
+If you don't want to send different notification events for Incidents and Events, you  should enable this setting:
+
+```python
+# Send 'incident:*' notification events for both Event and Incident if True
+NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS = True
+```
+
 ### Celery
 
 `fir_notifications` uses the FIR plugin `fir_celery`.
diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index 4b1603e3..ff91db8e 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -60,12 +60,39 @@ class Meta:
         index_together = ["user", "event", "method"]
 
 
-@notification_event('event:created', model_created, Incident, verbose_name=_('Event created'),
-                    section=_('Event'))
-def event_created(sender, instance, **kwargs):
-    if instance.is_incident:
-        return None, None
-    return instance, instance.concerned_business_lines
+if not settings.NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS:
+    @notification_event('event:created', model_created, Incident, verbose_name=_('Event created'),
+                        section=_('Event'))
+    def event_created(sender, instance, **kwargs):
+        if instance.is_incident:
+            return None, None
+        return instance, instance.concerned_business_lines
+
+
+    @notification_event('event:updated', model_updated, Incident, verbose_name=_('Event updated'),
+                        section=_('Event'))
+    def event_updated(sender, instance, **kwargs):
+        if instance.is_incident:
+            return None, None
+        return instance, instance.concerned_business_lines
+
+
+    @notification_event('event:commented', post_save, Comments, verbose_name=_('Event commented'),
+                        section=_('Event'))
+    def event_commented(sender, instance, **kwargs):
+        if not instance.incident and instance.incident.is_incident:
+            return None, None
+        if instance.action.name in ['Opened', 'Blocked', 'Closed']:
+            return None, None
+        return instance, instance.incident.concerned_business_lines
+
+
+    @notification_event('event:status_changed', model_status_changed, Incident, verbose_name=_('Event status changed'),
+                        section=_('Event'))
+    def event_status_changed(sender, instance, **kwargs):
+        if instance.is_incident:
+            return None, None
+        return instance, instance.concerned_business_lines
 
 
 @notification_event('incident:created', model_created, Incident, verbose_name=_('Incident created'),
@@ -76,53 +103,27 @@ def incident_created(sender, instance, **kwargs):
     return instance, instance.concerned_business_lines
 
 
-@notification_event('event:updated', model_updated, Incident, verbose_name=_('Event updated'),
-                    section=_('Event'))
-def event_updated(sender, instance, **kwargs):
-    if instance.is_incident:
-        return None, None
-    return instance, instance.concerned_business_lines
-
-
 @notification_event('incident:updated', model_updated, Incident, verbose_name=_('Incident updated'),
                     section=_('Incident'))
 def incident_updated(sender, instance, **kwargs):
-    if not instance.is_incident:
+    if not settings.NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS and not instance.is_incident:
         return None, None
     return instance, instance.concerned_business_lines
 
 
-@notification_event('event:commented', post_save, Comments, verbose_name=_('Event commented'),
-                    section=_('Event'))
-def event_commented(sender, instance, **kwargs):
-    if not instance.incident and instance.incident.is_incident:
-        return None, None
-    if instance.action.name in ['Opened', 'Blocked', 'Closed']:
-        return None, None
-    return instance, instance.incident.concerned_business_lines
-
-
 @notification_event('incident:commented', post_save, Comments, verbose_name=_('Incident commented'),
                     section=_('Incident'))
 def incident_commented(sender, instance, **kwargs):
-    if not instance.incident and not instance.incident.is_incident:
+    if not instance.incident and not settings.NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS and not instance.incident.is_incident:
         return None, None
     if instance.action.name in ['Opened', 'Blocked', 'Closed']:
         return None, None
     return instance, instance.incident.concerned_business_lines
 
 
-@notification_event('event:status_changed', model_status_changed, Incident, verbose_name=_('Event status changed'),
-                    section=_('Event'))
-def event_status_changed(sender, instance, **kwargs):
-    if instance.is_incident:
-        return None, None
-    return instance, instance.concerned_business_lines
-
-
 @notification_event('incident:status_changed', model_status_changed, Incident, verbose_name=_('Incident status changed'),
                     section=_('Incident'))
 def incident_status_changed(sender, instance, **kwargs):
-    if not instance.is_incident:
+    if not settings.NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS and not instance.is_incident:
         return None, None
     return instance, instance.concerned_business_lines
\ No newline at end of file

From 3649fa2b14f8bb22fc6f1fc7420fa605b77aa841 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 20 Jan 2017 10:31:52 +0100
Subject: [PATCH 42/66] Notifications: nove EXTERNAL_URL setting from base to
 production sample

---
 fir/config/base.py                 | 3 ---
 fir/config/production.py.sample    | 3 +++
 fir_notifications/methods/utils.py | 8 +++++---
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/fir/config/base.py b/fir/config/base.py
index b98d9bdd..4c72550a 100755
--- a/fir/config/base.py
+++ b/fir/config/base.py
@@ -156,9 +156,6 @@
     'CHANGE_PASSWORD': True
 }
 
-# External URL of your FIR application (used in fir_notification to render full URIs in templates)
-EXTERNAL_URL = 'http://fir.example.com'
-
 # Put notification events you don't want in this tuple
 # Example: NOTIFICATIONS_DISABLED_EVENTS = ('event:created', 'incident:created')
 NOTIFICATIONS_DISABLED_EVENTS = ()
diff --git a/fir/config/production.py.sample b/fir/config/production.py.sample
index 78345c74..5fff4821 100755
--- a/fir/config/production.py.sample
+++ b/fir/config/production.py.sample
@@ -71,3 +71,6 @@ LOGGING = {
         },
     },
 }
+
+# External URL of your FIR application (used in fir_notification to render full URIs in templates)
+EXTERNAL_URL = 'http://fir.example.com'
diff --git a/fir_notifications/methods/utils.py b/fir_notifications/methods/utils.py
index 434414ff..dff74030 100644
--- a/fir_notifications/methods/utils.py
+++ b/fir_notifications/methods/utils.py
@@ -3,9 +3,11 @@
 
 class FakeRequest(object):
     def __init__(self):
-        self.base = settings.EXTERNAL_URL
-        if self.base.endswith('/'):
-            self.base = self.base[:-1]
+        self.base = ""
+        if hasattr(settings, 'EXTERNAL_URL'):
+            self.base = settings.EXTERNAL_URL
+            if self.base.endswith('/'):
+                self.base = self.base[:-1]
 
     def build_absolute_uri(self, location):
         return "{}{}".format(self.base, location)

From 70035a2eb3a77963f749306910ef6df643a7689d Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 20 Jan 2017 10:54:39 +0100
Subject: [PATCH 43/66] Notifications: move email logic into fir_email

---
 fir_email/helpers.py               | 36 +++++++++++++++++++-----------
 fir_notifications/methods/email.py | 22 ++++--------------
 2 files changed, 27 insertions(+), 31 deletions(-)

diff --git a/fir_email/helpers.py b/fir_email/helpers.py
index bec0f9ec..804f298d 100644
--- a/fir_email/helpers.py
+++ b/fir_email/helpers.py
@@ -13,7 +13,7 @@ def _combine_with_settings(values, setting):
     return values
 
 
-def send(request, to, subject, body, behalf=None, cc='', bcc=''):
+def prepare_email_message(to, subject, body, behalf=None, cc=None, bcc=None, request=None):
     reply_to = {}
 
     if hasattr(settings, 'REPLY_TO'):
@@ -22,23 +22,33 @@ def send(request, to, subject, body, behalf=None, cc='', bcc=''):
     if behalf is None and hasattr(settings, 'EMAIL_FROM'):
         behalf = settings.EMAIL_FROM
 
-    cc = _combine_with_settings(cc, 'EMAIL_CC')
-    bcc = _combine_with_settings(bcc, 'EMAIL_BCC')
+    if not isinstance(to, (tuple, list)):
+        to = to.split(';')
 
-    e = EmailMultiAlternatives(
+    email_message = EmailMultiAlternatives(
         subject=subject,
+        body=body,
         from_email=behalf,
-        to=to.split(';'),
+        to=to,
         cc=cc,
         bcc=bcc,
         headers=reply_to
     )
-    e.attach_alternative(markdown2.markdown(
-            body,
-            extras=["link-patterns", "tables", "code-friendly"],
-            link_patterns=registry.link_patterns(request),
-            safe_mode=True
-        ),
+    email_message.attach_alternative(markdown2.markdown(
+        body,
+        extras=["link-patterns", "tables", "code-friendly"],
+        link_patterns=registry.link_patterns(request),
+        safe_mode=True
+    ),
         'text/html')
-    e.content_subtype = 'html'
-    e.send()
+
+    return email_message
+
+
+def send(request, to, subject, body, behalf=None, cc='', bcc=''):
+
+    cc = _combine_with_settings(cc, 'EMAIL_CC')
+    bcc = _combine_with_settings(bcc, 'EMAIL_BCC')
+
+    email_message = prepare_email_message(to, subject, body, behalf=behalf, cc=cc, bcc=bcc, request=request)
+    email_message.send()
diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index 50e28581..1dd488a8 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -1,13 +1,12 @@
-import markdown2
 from django import forms
 from django.conf import settings
 from django.core import mail
 from django.utils.translation import ugettext_lazy as _
 
+from fir_email.helpers import prepare_email_message
 
 from fir_notifications.methods import NotificationMethod
 from fir_notifications.methods.utils import request
-from fir_plugins.links import registry as link_registry
 
 
 class EmailMethod(NotificationMethod):
@@ -26,11 +25,6 @@ def __init__(self):
                                                           help_text=_('Encryption certificate in PEM format.'))
 
     def send(self, event, users, instance, paths):
-        from_address = settings.EMAIL_FROM
-        reply_to = {}
-        if hasattr(settings, 'REPLY_TO'):
-            reply_to = {'Reply-To': settings.REPLY_TO,
-                        'Return-Path': settings.REPLY_TO}
         messages = []
         for user, templates in users.items():
             if not self.enabled(event, user, paths) or not user.email:
@@ -39,17 +33,9 @@ def send(self, event, users, instance, paths):
             if template is None:
                 continue
             params = self.prepare(template, instance)
-            e = mail.EmailMultiAlternatives(
-                subject=params['subject'],
-                body=params['description'],
-                from_email=from_address,
-                to=[user.email, ],
-                headers=reply_to
-            )
-            e.attach_alternative(markdown2.markdown(params['description'], extras=["link-patterns"],
-                                                    link_patterns=link_registry.link_patterns(request), safe_mode=True),
-                                 'text/html')
-            messages.append(e)
+            email_message = prepare_email_message([user.email, ], params['subject'], params['description'],
+                                                  request=request)
+            messages.append(email_message)
         if len(messages):
             connection = mail.get_connection()
             connection.send_messages(messages)

From d049081b6d71ccf0b9ac99c0f84962e7972a2a57 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 20 Jan 2017 11:19:56 +0100
Subject: [PATCH 44/66] Notifications: fix xmpppy requirement install

---
 fir_notifications/methods/jabber.py | 2 +-
 fir_notifications/requirements.txt  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fir_notifications/methods/jabber.py b/fir_notifications/methods/jabber.py
index d5b9abf2..f41de560 100644
--- a/fir_notifications/methods/jabber.py
+++ b/fir_notifications/methods/jabber.py
@@ -1,7 +1,7 @@
 import markdown2
 from django.conf import settings
 
-import xmpppy as xmpp
+import xmpp
 from django import forms
 
 from fir_notifications.methods import NotificationMethod
diff --git a/fir_notifications/requirements.txt b/fir_notifications/requirements.txt
index 78bd362e..da8c3560 100644
--- a/fir_notifications/requirements.txt
+++ b/fir_notifications/requirements.txt
@@ -1 +1 @@
--e git+https://github.com/ArchipelProject/xmpppy.git@288b280c6ec534c100bfee871daa3bb707467a1a#egg=xmpppy
\ No newline at end of file
+https://github.com/ArchipelProject/xmpppy/zipball/288b280c6ec534c100bfee871daa3bb707467a1a
\ No newline at end of file

From 46784ac1d19fc4a0b7d7c29383c77f1bcd5d9826 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 20 Jan 2017 11:34:35 +0100
Subject: [PATCH 45/66] Move S/MIME requirements file into fir_email

---
 {fir_notifications => fir_email}/requirements_smime.txt | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename {fir_notifications => fir_email}/requirements_smime.txt (100%)

diff --git a/fir_notifications/requirements_smime.txt b/fir_email/requirements_smime.txt
similarity index 100%
rename from fir_notifications/requirements_smime.txt
rename to fir_email/requirements_smime.txt

From e3a243a88c7b50da85bdf134ede41c462852b5da Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 15:49:04 +0100
Subject: [PATCH 46/66] Notifications: remove S/MIME stuff from
 fir_notifications

---
 fir_notifications/forms.py         | 12 -----------
 fir_notifications/methods/email.py | 32 ------------------------------
 2 files changed, 44 deletions(-)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index b55930cf..88834a0d 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -33,18 +33,6 @@ class Meta:
         fields = '__all__'
 
 
-class EmailMethodConfigurationForm(MethodConfigurationForm):
-    def save(self, *args, **kwargs):
-        if self.user is None or not self.user.email:
-            return None
-        try:
-            from djembe.models import Identity
-        except ImportError:
-            return None
-        config, created = Identity.objects.update_or_create(address=self.user.email, defaults=self.cleaned_data)
-        return config
-
-
 class NotificationPreferenceFormset(forms.BaseInlineFormSet):
     def __init__(self, *args, **kwargs):
         self.notifications = OrderedDict()
diff --git a/fir_notifications/methods/email.py b/fir_notifications/methods/email.py
index 1dd488a8..49f948cb 100644
--- a/fir_notifications/methods/email.py
+++ b/fir_notifications/methods/email.py
@@ -1,7 +1,5 @@
-from django import forms
 from django.conf import settings
 from django.core import mail
-from django.utils.translation import ugettext_lazy as _
 
 from fir_email.helpers import prepare_email_message
 
@@ -19,10 +17,6 @@ def __init__(self):
         super(EmailMethod, self).__init__()
         if hasattr(settings, 'EMAIL_FROM') and settings.EMAIL_FROM is not None:
             self.server_configured = True
-        if 'djembe' in settings.INSTALLED_APPS:
-            self.options['certificate'] = forms.CharField(required=False, label=_('Certificate'),
-                                                          widget=forms.Textarea(attrs={'cols': 60, 'rows': 15}),
-                                                          help_text=_('Encryption certificate in PEM format.'))
 
     def send(self, event, users, instance, paths):
         messages = []
@@ -42,29 +36,3 @@ def send(self, event, users, instance, paths):
 
     def configured(self, user):
         return super(EmailMethod, self).configured(user) and user.email is not None
-
-    def _get_configuration(self, user):
-        if not user.email:
-            return {}
-        try:
-            from djembe.models import Identity
-        except ImportError:
-            return {}
-        try:
-            identity = Identity.objects.get(address=user.email)
-        except Identity.DoesNotExist:
-            return {}
-        except Identity.MultipleObjectsReturned:
-            identity = Identity.objects.filter(address=user.email).first()
-        return {'certificate': identity.certificate}
-
-    def form(self, *args, **kwargs):
-        from fir_notifications.forms import EmailMethodConfigurationForm
-        if not len(self.options):
-            return None
-        user = kwargs.pop('user', None)
-        if user is not None:
-            kwargs['initial'] = self._get_configuration(user)
-            kwargs['user'] = user
-        kwargs['method'] = self
-        return EmailMethodConfigurationForm(*args, **kwargs)

From 3892eece4c2d40dd696a2e7548e2eb5ba492b7fd Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 15:49:33 +0100
Subject: [PATCH 47/66] Notifications: remove S/MIME doc from fir_notifications

---
 fir_notifications/README.md | 56 +------------------------------------
 1 file changed, 1 insertion(+), 55 deletions(-)

diff --git a/fir_notifications/README.md b/fir_notifications/README.md
index 51aeb53a..3e81016f 100644
--- a/fir_notifications/README.md
+++ b/fir_notifications/README.md
@@ -71,61 +71,7 @@ EXTERNAL_URL = 'https://fir.example.com'
 
 ### Email notifications
 
-You have to configure [Django email backend](https://docs.djangoproject.com/en/1.9/topics/email/).
-
-In addition, `fir_notifications` uses two settings defined in `fir_email`:
-
-``` python
-# From address (required)
-EMAIL_FROM = 'fir@example.com'
-# Reply to address (optional)
-REPLY_TO = None
-```
-
-#### S/MIME
-
-To send signed/encrypted email notifications with S/MIME to users, install and configure [django-djembe](https://github.com/cabincode/django-djembe) and add it in your *installed_apps.txt*.
-
-The following configuration example from the Djembe Readme can help you:
-
-``` bash
-(fir-env)$ pip install -r fir_notifications/requirements_smime.txt
-(fir-env)$ python manage.py migrate djembe
-```
-
-In *$FIR_HOME/fir/config/installed_app.txt*, add:
-
-```
-djembe
-```
-
-Change your email backend in your settings:
-
-``` python
-EMAIL_BACKEND = 'djembe.backends.EncryptingSMTPBackend'
-```
-
-To use a cipher other than the default AES-256, specify it in your settings `DJEMBE_CIPHER`:
-
-
-``` python
-DJEMBE_CIPHER = 'des_ede3_cbc'  # triple DES
-```
-The intersection of M2Crypto's ciphers and RFC 3851 are:
-
-* `des_ede3_cbc` (required by the RFC)
-* `aes_128_cbc` (recommended, not required, by the RFC)
-* `aes_192_cbc` (recommended, not required, by the RFC)
-* `aes_256_cbc` (recommended, not required, by the RFC)
-* `rc2_40_cbc` (RFC requires support, but it's weak -- don't use it)
-
-RFC 5751 requires AES-128, and indicates that higher key lengths are of
-course the future. It marks tripleDES with "SHOULD-", meaning it's on its
-way out.
-
-To create signed notifications, in the admin site (*Djembe > Identities*), supply both a certificate and a private key which must not have a passphrase, with an `Address` that is the same as your setting `EMAIL_FROM`. Any mail sent *from* this Identity's address will be signed with the private key.
-
-User certificates will be added from the user profile in FIR (*Configure Email*).
+Follow the `fir_email` [README](fir_email/README.md).
 
 ### Jabber (XMPP) notifications
 

From 5f4f17ba06cce83f8e146cd4899dc5fac8de24ea Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 15:51:51 +0100
Subject: [PATCH 48/66] Email: check Django-djembe configuration helper

---
 fir_email/utils.py | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 fir_email/utils.py

diff --git a/fir_email/utils.py b/fir_email/utils.py
new file mode 100644
index 00000000..7a46b2f8
--- /dev/null
+++ b/fir_email/utils.py
@@ -0,0 +1,8 @@
+from django.conf import settings
+
+
+def check_smime_status():
+    if 'djembe' in settings.INSTALLED_APPS \
+            and settings.EMAIL_BACKEND == 'djembe.backends.EncryptingSMTPBackend':
+        return True
+    return False

From bd30af388c4ff3b7872065db48ee27b321738d61 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 16:03:34 +0100
Subject: [PATCH 49/66] Email: User S/MIME certificate form

---
 fir_email/forms.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 fir_email/forms.py

diff --git a/fir_email/forms.py b/fir_email/forms.py
new file mode 100644
index 00000000..84208c74
--- /dev/null
+++ b/fir_email/forms.py
@@ -0,0 +1,54 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from fir_email.utils import check_smime_status
+
+
+class SMIMECertificateForm(forms.Form):
+    certificate = forms.CharField(required=False, label=_('Certificate'),
+                                  widget=forms.Textarea(attrs={'cols': 60, 'rows': 15}),
+                                  help_text=_('Encryption certificate in PEM format.'))
+
+    def __init__(self, *args, **kwargs):
+        self.user = kwargs.pop('user', None)
+        if self.user is not None and self.user.email and 'initial' not in kwargs:
+            kwargs['initial'] = self._get_certificate()
+        super(SMIMECertificateForm, self).__init__(*args, **kwargs)
+
+    def _get_certificate(self):
+        data = {}
+        try:
+            from djembe.models import Identity
+        except ImportError:
+            return data
+        try:
+            identity = Identity.objects.get(address=self.user.email)
+        except Identity.DoesNotExist:
+            return data
+        except Identity.MultipleObjectsReturned:
+            identity = Identity.objects.filter(address=self.user.email).first()
+        data = {'certificate': identity.certificate}
+        return data
+
+    def clean_certificate(self):
+        if not check_smime_status():
+            raise forms.ValidationError(_('Improperly configured S/MIME: Email backend is incompatible'))
+        try:
+            from M2Crypto import X509
+            certificate = self.cleaned_data['certificate']
+            X509.load_cert_string(str(certificate))
+        except ImportError:
+            raise forms.ValidationError(_('Improperly configured S/MIME: missing dependencies'))
+        except X509.X509Error:
+            raise forms.ValidationError(_('Invalid certificate: unknown format'))
+        return certificate
+
+    def save(self, *args, **kwargs):
+        if self.user is None or not self.user.email:
+            return None
+        try:
+            from djembe.models import Identity
+        except ImportError:
+            return None
+        config, created = Identity.objects.update_or_create(address=self.user.email, defaults=self.cleaned_data)
+        return config
\ No newline at end of file

From 32441f0fcfa5b60a53aeb104fe961c3e1262f3e8 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 16:04:04 +0100
Subject: [PATCH 50/66] Email: User certificate templates

---
 .../plugins/user_profile_actions.html         |  2 +
 .../fir_email/smime_profile_action.html       | 56 +++++++++++++++++++
 fir_email/templatetags/__init__.py            |  0
 fir_email/templatetags/smime.py               | 14 +++++
 4 files changed, 72 insertions(+)
 create mode 100644 fir_email/templates/fir_email/plugins/user_profile_actions.html
 create mode 100644 fir_email/templates/fir_email/smime_profile_action.html
 create mode 100644 fir_email/templatetags/__init__.py
 create mode 100644 fir_email/templatetags/smime.py

diff --git a/fir_email/templates/fir_email/plugins/user_profile_actions.html b/fir_email/templates/fir_email/plugins/user_profile_actions.html
new file mode 100644
index 00000000..dd8ddda0
--- /dev/null
+++ b/fir_email/templates/fir_email/plugins/user_profile_actions.html
@@ -0,0 +1,2 @@
+{% load smime %}
+{% smime_profile_action %}
\ No newline at end of file
diff --git a/fir_email/templates/fir_email/smime_profile_action.html b/fir_email/templates/fir_email/smime_profile_action.html
new file mode 100644
index 00000000..e33a5aac
--- /dev/null
+++ b/fir_email/templates/fir_email/smime_profile_action.html
@@ -0,0 +1,56 @@
+{% load i18n %}
+{% load add_css_class %}
+
+{% if smime_status %}
+<li><a href="#" id="user_add_smime" data-toggle='modal' data-target='#add_smime'><i class="glyphicon glyphicon-lock"></i>{% blocktrans %}Set S/MIME certificate{% endblocktrans %}</a></li>
+
+<div id="add_smime" class="modal fade smime-certificate-form" tabindex="-1" role="dialog" aria-labelledby="add_smime_label" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <form id="add_smime_form" method="POST" action="{% url 'email:user-certificate' %}">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+                    <h4 id="add_smime_label" class="modal-title">{% trans 'Set certificate' %}</h4>
+                </div>
+                <div class="modal-body">
+                    <div>
+                        {% csrf_token %}
+                        {% for field in form %}
+                        <div class="form-group row">
+                            <div class="col-sm-4">
+                                {{ field.label_tag }}
+                            </div>
+                            <div class="col-sm-8">
+                                {{ field|add_css_class:"form-control" }}
+                                <span class='help-block'>
+                                    {% if not field.errors and field.help_text %}
+                                        {{field.help_text}}
+                                    {% endif %}
+                                    {% for error in field.errors %}
+                                        {{error}}
+                                    {% endfor %}
+                                </span>
+                            </div>
+                        </div>
+                        {% endfor %}
+                    </div>
+                </div>
+
+                <div class="modal-footer">
+                    <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">{%  trans "Cancel" %}</button>
+                    <button type="submit" class="btn btn-primary">{%  trans "Save" %}</button>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+<script>
+        window.onload = function(){
+            $('.notification-method-form').on('fir.form.success', function (event) {
+                console.log(this);
+                $(this).modal('hide');
+                event.stopPropagation();
+            });
+        };
+    </script>
+{% endif %}
\ No newline at end of file
diff --git a/fir_email/templatetags/__init__.py b/fir_email/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fir_email/templatetags/smime.py b/fir_email/templatetags/smime.py
new file mode 100644
index 00000000..e784284e
--- /dev/null
+++ b/fir_email/templatetags/smime.py
@@ -0,0 +1,14 @@
+from django import template
+
+from fir_email.utils import check_smime_status
+from fir_email.forms import SMIMECertificateForm
+
+register = template.Library()
+
+
+@register.inclusion_tag('fir_email/smime_profile_action.html', takes_context=True)
+def smime_profile_action(context):
+    if check_smime_status() and context.request.user.email:
+        form = SMIMECertificateForm(user=context.request.user)
+        return {'form': form, 'smime_status': True}
+    return {'form': None, 'smime_status': False}
\ No newline at end of file

From d80044594626cdc30bf4982886120903aacf5675 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 16:05:10 +0100
Subject: [PATCH 51/66] Email: User certificate view

---
 fir_email/urls.py  |  9 +++++++++
 fir_email/views.py | 18 ++++++++++++++++++
 2 files changed, 27 insertions(+)
 create mode 100644 fir_email/urls.py
 create mode 100644 fir_email/views.py

diff --git a/fir_email/urls.py b/fir_email/urls.py
new file mode 100644
index 00000000..edcf89d0
--- /dev/null
+++ b/fir_email/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import url
+from fir_email.utils import check_smime_status
+from fir_email import views
+
+
+urlpatterns = []
+
+if check_smime_status():
+    urlpatterns.append(url(r'^user/certificate/$', views.smime_configuration, name='user-certificate'))
diff --git a/fir_email/views.py b/fir_email/views.py
new file mode 100644
index 00000000..b1ed33f6
--- /dev/null
+++ b/fir_email/views.py
@@ -0,0 +1,18 @@
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import redirect
+from django.views.decorators.http import require_POST
+from django.contrib import messages
+
+from fir_email.forms import SMIMECertificateForm
+
+
+@require_POST
+@login_required
+def smime_configuration(request):
+    form = SMIMECertificateForm(request.POST, user=request.user)
+    if form.is_valid():
+        form.save()
+    else:
+        for error in form.errors.items():
+            messages.error(request, error[1])
+    return redirect('user:profile')
\ No newline at end of file

From bff417d1be30c7158af1378379eb9d9f9b51c91c Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 16:05:27 +0100
Subject: [PATCH 52/66] Email: Initial readme

---
 fir_email/README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)
 create mode 100644 fir_email/README.md

diff --git a/fir_email/README.md b/fir_email/README.md
new file mode 100644
index 00000000..29aa19a6
--- /dev/null
+++ b/fir_email/README.md
@@ -0,0 +1,76 @@
+# Email helpers module
+
+## Configure FIR to send emails
+
+Follow the Django docs: [Django email backend](https://docs.djangoproject.com/en/1.9/topics/email/).
+
+In addition, you have to configure two settings:
+
+```python
+# From address (required, string)
+EMAIL_FROM = 'fir@example.com'
+# Reply to address (optional, string)
+REPLY_TO = None
+```
+
+## Adding CC and BCC recipients (for `fir_alerting` and `fir_abuse`)
+
+You can force FIR to add CC and BCC recipients by configuring these settings:
+
+```python
+EMAIL_CC = ['cc@example.com',]
+EMAIL_BCC = ['bcc@example.com',]
+```
+
+## Using S/MIME
+
+To send signed/encrypted emails with S/MIME to users, install and configure [django-djembe](https://github.com/cabincode/django-djembe) and add it in your *installed_apps.txt*.
+
+The following configuration example from the Djembe Readme can help you:
+
+### Install
+
+```bash
+(fir-env)$ pip install -r fir_email/requirements_smime.txt
+(fir-env)$ python manage.py migrate djembe
+```
+
+In *$FIR_HOME/fir/config/installed_app.txt*, add:
+
+```
+djembe
+```
+
+Change your email backend in your settings:
+
+```python
+EMAIL_BACKEND = 'djembe.backends.EncryptingSMTPBackend'
+```
+
+### Ciphers
+
+To use a cipher other than the default AES-256, specify it in your settings `DJEMBE_CIPHER`:
+
+
+```python
+DJEMBE_CIPHER = 'des_ede3_cbc'  # triple DES
+```
+The intersection of M2Crypto's ciphers and RFC 3851 are:
+
+* `des_ede3_cbc` (required by the RFC)
+* `aes_128_cbc` (recommended, not required, by the RFC)
+* `aes_192_cbc` (recommended, not required, by the RFC)
+* `aes_256_cbc` (recommended, not required, by the RFC)
+* `rc2_40_cbc` (RFC requires support, but it's weak -- don't use it)
+
+RFC 5751 requires AES-128, and indicates that higher key lengths are of
+course the future. It marks tripleDES with "SHOULD-", meaning it's on its
+way out.
+
+### Signed email
+
+To create signed email, in the admin site (*Djembe > Identities*), supply both a certificate and a private key which must not have a passphrase, with an `Address` that is the same as your setting `EMAIL_FROM`. Any mail sent *from* this Identity's address will be signed with the private key.
+
+### User certificates (email encryption)
+
+User certificates will be added from the user profile in FIR (*Set S/MIME certificate*).

From b3650f264f41e337d64fa691f61e9aefb32e804c Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Wed, 25 Jan 2017 16:06:18 +0100
Subject: [PATCH 53/66] Notifications: use Django messages framework to report
 form errors (method configuration)

---
 fir_notifications/views.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/fir_notifications/views.py b/fir_notifications/views.py
index 100cedd9..70a55105 100644
--- a/fir_notifications/views.py
+++ b/fir_notifications/views.py
@@ -1,5 +1,6 @@
 from django.contrib.auth.decorators import login_required
 from django import forms
+from django.contrib import messages
 from django.shortcuts import redirect, render
 from django.views.decorators.http import require_POST
 from django.contrib.auth import get_user_model
@@ -20,6 +21,9 @@ def method_configuration(request, method):
     form = method_object.form(request.POST, user=request.user)
     if form.is_valid():
         form.save()
+    else:
+        for error in form.errors.items():
+            messages.error(request, error[1])
     return redirect('user:profile')
 
 

From 727da0d2ccade5b4f23685c012bcb2daad4ccbe0 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Thu, 26 Jan 2017 06:45:19 +0100
Subject: [PATCH 54/66] Email: Add fir_email as a FIR core app

---
 fir/config/base.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fir/config/base.py b/fir/config/base.py
index 4c72550a..4397e4fa 100755
--- a/fir/config/base.py
+++ b/fir/config/base.py
@@ -95,6 +95,7 @@
     'incidents',
     'fir_artifacts',
     'treebeard',
+    'fir_email'
 )
 
 apps_file = os.path.join(BASE_DIR, 'fir', 'config', 'installed_apps.txt')

From 7b5ad3cf89abfcd043020b89788d134141621d94 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Thu, 26 Jan 2017 06:51:07 +0100
Subject: [PATCH 55/66] Email: Add French translation

---
 fir_email/locale/fr/LC_MESSAGES/django.po | 55 +++++++++++++++++++++++
 1 file changed, 55 insertions(+)
 create mode 100644 fir_email/locale/fr/LC_MESSAGES/django.po

diff --git a/fir_email/locale/fr/LC_MESSAGES/django.po b/fir_email/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..8caf4a2e
--- /dev/null
+++ b/fir_email/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,55 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-01-26 06:46+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: forms.py:8
+msgid "Certificate"
+msgstr "Certificat"
+
+#: forms.py:10
+msgid "Encryption certificate in PEM format."
+msgstr "Certificat de chiffrement au format PEM."
+
+#: forms.py:35
+msgid "Improperly configured S/MIME: Email backend is incompatible"
+msgstr "Mauvaise configuration de S/MIME : le moteur de messagerie est incompatible"
+
+#: forms.py:41
+msgid "Improperly configured S/MIME: missing dependencies"
+msgstr "auvaise configuration de S/MIME : dépendances manquantes"
+
+#: forms.py:43
+msgid "Invalid certificate: unknown format"
+msgstr "Certificat invalide : format inconnu"
+
+#: templates/fir_email/smime_profile_action.html:5
+msgid "Set S/MIME certificate"
+msgstr "Définir le certificat S/MIME"
+
+#: templates/fir_email/smime_profile_action.html:13
+msgid "Set certificate"
+msgstr "Définir le certificat"
+
+#: templates/fir_email/smime_profile_action.html:40
+msgid "Cancel"
+msgstr "Annuler"
+
+#: templates/fir_email/smime_profile_action.html:41
+msgid "Save"
+msgstr "Enregistrer"

From ba108cdca589f920645a93b006bef910024f89d4 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Thu, 26 Jan 2017 06:52:42 +0100
Subject: [PATCH 56/66] Notifications: Update French translation

---
 .../locale/fr/LC_MESSAGES/django.po           | 52 +++++++------------
 1 file changed, 20 insertions(+), 32 deletions(-)

diff --git a/fir_notifications/locale/fr/LC_MESSAGES/django.po b/fir_notifications/locale/fr/LC_MESSAGES/django.po
index 89980538..bb09e748 100644
--- a/fir_notifications/locale/fr/LC_MESSAGES/django.po
+++ b/fir_notifications/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-01-17 08:48+0100\n"
+"POT-Creation-Date: 2017-01-26 06:51+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -36,14 +36,6 @@ msgstr ""
 msgid "Configure %(method)s"
 msgstr "Configurer %(method)s"
 
-#: methods/email.py:24
-msgid "Certificate"
-msgstr "Certificat"
-
-#: methods/email.py:26
-msgid "Encryption certificate in PEM format."
-msgstr "Certificat de chiffrement au format PEM"
-
 #: methods/jabber.py:28
 msgid "Jabber ID"
 msgstr ""
@@ -108,47 +100,43 @@ msgstr "préférence de notification"
 msgid "notification preferences"
 msgstr "préférences de notification"
 
-#: models.py:63
+#: models.py:64
 msgid "Event created"
 msgstr "Événement créé"
 
-#: models.py:64 models.py:80 models.py:96 models.py:118
+#: models.py:65 models.py:73 models.py:81 models.py:91
 msgid "Event"
 msgstr "Événement"
 
-#: models.py:71
+#: models.py:72
+msgid "Event updated"
+msgstr "Événement mis à jour"
+
+#: models.py:80
+msgid "Event commented"
+msgstr "Événement commenté"
+
+#: models.py:90
+msgid "Event status changed"
+msgstr "Statut de l'événement changé"
+
+#: models.py:98
 msgid "Incident created"
 msgstr "Incident créé"
 
-#: models.py:72 models.py:88 models.py:107 models.py:126
+#: models.py:99 models.py:107 models.py:115 models.py:125
 msgid "Incident"
 msgstr "Incident"
 
-#: models.py:79
-msgid "Event updated"
-msgstr "Événement mis à jour"
-
-#: models.py:87
+#: models.py:106
 msgid "Incident updated"
 msgstr "Incident mis à jour"
 
-#: models.py:95
-#| msgid "Event created"
-msgid "Event commented"
-msgstr "Événement commenté"
-
-#: models.py:106
-#| msgid "Incident created"
+#: models.py:114
 msgid "Incident commented"
 msgstr "Incident commenté"
 
-#: models.py:117
-#| msgid "Event created"
-msgid "Event status changed"
-msgstr "Statut de l'événement changé"
-
-#: models.py:125
-#| msgid "Incident created"
+#: models.py:124
 msgid "Incident status changed"
 msgstr "Statut de l'incident changé"
 

From 2630d2c1a951904009994fab9655951cff546e10 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:11:33 +0100
Subject: [PATCH 57/66] Add Select2 static files to user profile page

---
 incidents/templates/user/profile.html | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/incidents/templates/user/profile.html b/incidents/templates/user/profile.html
index 9e9d635a..16c473db 100644
--- a/incidents/templates/user/profile.html
+++ b/incidents/templates/user/profile.html
@@ -4,6 +4,14 @@
 {% load staticfiles %}
 {% load fir_plugins %}
 
+{% block custom_css %}
+<link href="{% static "select/select2.css" %}" rel="stylesheet"/>
+<link href="{% static "select/select2-bootstrap.css" %}" rel="stylesheet"/>
+{% endblock %}
+
+{% block custom_js %}
+<script src="{% static "select/select2.min.js" %}"></script>
+{% endblock %}
 
 {% block content %}
     <div id='details-actions-all'>

From 9d841ef512c28ceaf0af409570b6f4a068edde66 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:12:22 +0100
Subject: [PATCH 58/66] Notifications: refactor user preferences UI

---
 fir_notifications/forms.py                    | 103 +++++-------------
 .../static/fir_notifications/notifications.js |  67 ++++++++++++
 .../plugins/user_profile.html                 |   2 +-
 .../fir_notifications/subscribe.html          |  42 +++++++
 .../fir_notifications/subscriptions.html      |  43 ++++++++
 .../templatetags/notifications.py             |  15 +++
 fir_notifications/urls.py                     |   7 +-
 fir_notifications/views.py                    |  70 +++++++-----
 8 files changed, 244 insertions(+), 105 deletions(-)
 create mode 100644 fir_notifications/static/fir_notifications/notifications.js
 create mode 100644 fir_notifications/templates/fir_notifications/subscribe.html
 create mode 100644 fir_notifications/templates/fir_notifications/subscriptions.html

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index 88834a0d..df343f2e 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -1,11 +1,12 @@
 import json
-from collections import OrderedDict
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
+from incidents.models import BusinessLine
+
 from fir_notifications.registry import registry
-from fir_notifications.models import MethodConfiguration
+from fir_notifications.models import MethodConfiguration, NotificationPreference
 
 
 class MethodConfigurationForm(forms.Form):
@@ -33,82 +34,32 @@ class Meta:
         fields = '__all__'
 
 
-class NotificationPreferenceFormset(forms.BaseInlineFormSet):
+class NotificationPreferenceForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
-        self.notifications = OrderedDict()
-        for e, verbose_e in registry.events.items():
-            for m, verbose_m in registry.methods.items():
-                self.notifications["{}_{}".format(e, m)] = {'event': e,
-                                                            'verbose_event': verbose_e,
-                                                            'method': m,
-                                                            'verbose_method': verbose_m.verbose_name}
-        self.min_num = len(self.notifications)
-        self.max_num = len(self.notifications)
-        self.can_delete = False
+        self.user = kwargs.pop('user', None)
         instance = kwargs.get('instance', None)
+        if self.user is None and instance is not None:
+            self.user = instance.user
+        if instance is None and kwargs.get('data', None) is not None:
+            data = kwargs.get('data')
+            event = data.get('event', None)
+            method = data.get('method', None)
+            if event and method and self.user:
+                try:
+                    kwargs['instance'] = NotificationPreference.objects.get(user=self.user, event=event, method=method)
+                except (NotificationPreference.DoesNotExist, NotificationPreference.MultipleObjectsReturned):
+                    pass
+        super(NotificationPreferenceForm, self).__init__(*args, **kwargs)
+        self.fields['business_lines'].queryset = BusinessLine.authorization.for_user(self.user,
+                                                                                     'incidents.view_incidents')
         if instance is not None:
-            queryset = kwargs.get('queryset', None)
-            if queryset is None:
-                queryset = self.model._default_manager
-            qs = queryset.filter(event__in=registry.events.keys(), method__in= registry.methods.keys())
-            kwargs['queryset'] = qs
-        super(NotificationPreferenceFormset, self).__init__(*args, **kwargs)
-
-    def _construct_form(self, i, **kwargs):
-        method = None
-        event = None
-        if self.is_bound and i < self.initial_form_count():
-            pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
-            pk = self.data[pk_key]
-            pk_field = self.model._meta.pk
-            to_python = self._get_to_python(pk_field)
-            pk = to_python(pk)
-            instance = self._existing_object(pk)
-            notification = self.notifications.pop("{}_{}".format(instance.event, instance.method))
-            event = notification['verbose_event']
-            method = notification['verbose_method']
-            kwargs['instance'] = instance
-        if i < self.initial_form_count() and 'instance' not in kwargs:
-            instance = self.get_queryset()[i]
-            notification = self.notifications.pop("{}_{}".format(instance.event, instance.method))
-            event = notification['verbose_event']
-            method = notification['verbose_method']
-            kwargs['instance'] = self.get_queryset()[i]
-        if i >= self.initial_form_count() and self.notifications:
-            # Set initial values for extra forms
-            try:
-                key, initial = self.notifications.popitem()
-                event = initial['verbose_event']
-                method = initial['verbose_method']
-                kwargs['initial'] = {'event': initial['event'], 'method': initial['method']}
-            except IndexError:
-                pass
-        form = forms.BaseFormSet._construct_form(self, i, **kwargs)
-        if self.save_as_new:
-            # Remove the primary key from the form's data, we are only
-            # creating new instances
-            form.data[form.add_prefix(self._pk_field.name)] = None
+            self.fields['event'].disabled = True
+            self.fields['method'].disabled = True
 
-            # Remove the foreign key from the form's data
-            form.data[form.add_prefix(self.fk.name)] = None
+    event = forms.ChoiceField(choices=registry.get_event_choices(), label=_('Event'))
+    method = forms.ChoiceField(choices=registry.get_method_choices(), label=_('Method'))
+    business_lines = forms.ModelMultipleChoiceField(BusinessLine.objects.all(), label=_('Business lines'))
 
-            # Set the fk value here so that the form can do its validation.
-        fk_value = self.instance.pk
-        if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name:
-            fk_value = getattr(self.instance, self.fk.remote_field.field_name)
-            fk_value = getattr(fk_value, 'pk', fk_value)
-        setattr(form.instance, self.fk.get_attname(), fk_value)
-        setattr(form, 'get_notification_display', lambda: u"{} via {}".format(event.verbose_name, method))
-        setattr(form, 'get_event', lambda: event)
-        return form
-
-    @property
-    def labelled_forms(self):
-        fs_forms = {}
-        for form in self.forms:
-            label = form.get_event().section
-            if label not in fs_forms:
-                fs_forms[label] = []
-            fs_forms[label].append(form)
-            fs_forms[label] = sorted(fs_forms[label], key=lambda form: form.get_event().name)
-        return fs_forms
+    class Meta:
+        exclude = ('user', )
+        model = NotificationPreference
diff --git a/fir_notifications/static/fir_notifications/notifications.js b/fir_notifications/static/fir_notifications/notifications.js
new file mode 100644
index 00000000..9af314d6
--- /dev/null
+++ b/fir_notifications/static/fir_notifications/notifications.js
@@ -0,0 +1,67 @@
+
+
+function update_async_modals(elt) {
+	if(!elt) {
+		$('.modal-async').on('click', function (e) {
+			e.preventDefault();
+			ajax_action($(this), modal_action);
+		});
+	} else {
+		$(elt).find('.modal-async').on('click', function (e) {
+			e.preventDefault();
+			ajax_action($(this), modal_action);
+		});
+	}
+}
+
+
+$(function() {
+	update_async_modals();
+});
+
+function ajax_action(elt, callback) {
+    var target = elt.data('target');
+	$.ajax({
+		url: elt.data('url'),
+		headers: {'X-CSRFToken': getCookie('csrftoken')},
+	}).success(function(data) {
+		callback(data, target);
+	});
+}
+
+function modal_action(data, target_id) {
+    var target = $(target_id);
+	target.empty();
+	target.html(data);
+	$(target_id+" .modal").modal('show');
+	target.off('click', 'button[type=submit]');
+	target.find("select").select2({ dropdownAutoWidth: true, width: '100%' });
+
+	target.first().focus();
+	target.on('click', 'button[type=submit]', function(e) {
+		e.stopPropagation();
+		e.preventDefault();
+		var form = $(this).parents('form:first');
+		var data = form.serialize();
+		$.ajax({
+			type: 'POST',
+			url: form.attr('action'),
+			data: data,
+			headers: {'X-CSRFToken': getCookie('csrftoken')},
+			success: function (msg) {
+
+				if (msg.status == 'success') {
+					$(target_id+" .modal").modal('hide');
+					target.empty();
+					location.reload();
+				}
+
+				else if (msg.status == 'error') {
+					var html = $.parseHTML(msg.data);
+					$(target_id+" .modal .modal-body").html($(html).find('.modal-body'));
+					target.find("select").select2({ dropdownAutoWidth: true, width: '100%' });
+				}
+			}
+		});
+	});
+}
diff --git a/fir_notifications/templates/fir_notifications/plugins/user_profile.html b/fir_notifications/templates/fir_notifications/plugins/user_profile.html
index 5a810fa2..bc615cdb 100644
--- a/fir_notifications/templates/fir_notifications/plugins/user_profile.html
+++ b/fir_notifications/templates/fir_notifications/plugins/user_profile.html
@@ -1,3 +1,3 @@
-<div class='fir-async row' id='notifications-preferences' data-fetch-url="{% url 'notifications:preferences' %}">
+<div class='fir-async row' id='notifications-preferences' data-fetch-url="{% url 'notifications:subscriptions' %}">
 
 </div>
\ No newline at end of file
diff --git a/fir_notifications/templates/fir_notifications/subscribe.html b/fir_notifications/templates/fir_notifications/subscribe.html
new file mode 100644
index 00000000..27312b49
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/subscribe.html
@@ -0,0 +1,42 @@
+{% load  add_css_class %}
+{% load i18n %}
+<div id="subscribe_notifications" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="subscribe_notifications}_label" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <form id="subscribe_notifications_form" method="POST" action="{{ request.path }}">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+                    <h4 id="subscribe_notifications_label" class="modal-title">{% trans "Notification subscription" %}</h4>
+                </div>
+                <div class="modal-body">
+                    <div>
+                        {% csrf_token %}
+                        {% for field in form %}
+                        <div class="form-group row">
+                            <div class="col-sm-4">
+                                {{ field.label_tag }}
+                            </div>
+                            <div class="col-sm-8">
+                                {{ field|add_css_class:"form-control" }}
+                                <span class='help-block'>
+                                    {% if not field.errors and field.help_text %}
+                                        {{field.help_text}}
+                                    {% endif %}
+                                    {% for error in field.errors %}
+                                        {{error}}
+                                    {% endfor %}
+                                </span>
+                            </div>
+                        </div>
+                        {% endfor %}
+                    </div>
+                </div>
+
+                <div class="modal-footer">
+                    <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">{%  trans "Cancel" %}</button>
+                    <button type="submit" class="btn btn-primary">{%  trans "Save" %}</button>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/fir_notifications/templates/fir_notifications/subscriptions.html b/fir_notifications/templates/fir_notifications/subscriptions.html
new file mode 100644
index 00000000..38c77450
--- /dev/null
+++ b/fir_notifications/templates/fir_notifications/subscriptions.html
@@ -0,0 +1,43 @@
+{% load i18n %}
+{% load notifications %}
+{% load staticfiles %}
+
+<div class="col-sm-8 col-sm-offset-2">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">{% trans "Notification subscriptions" %}</h3>
+      </div>
+      <div class="panel-body">
+          <table class="table table-condensed">
+            <thead>
+                <tr>
+                    <th>{%  trans "Event" %}</th>
+                    <th>{%  trans "Method" %}</th>
+                    <th>{%  trans "Business lines" %}</th>
+                    <th></th>
+                    <th></th>
+                </tr>
+            </thead>
+            <tbody>
+              {% for preference in preferences %}
+                  <tr>
+                    <td>{{ preference.event|display_event }}</td>
+                    <td>{{ preference.method|display_method }}</td>
+                    <td>{{ preference.business_lines.all|join:', ' }}</td>
+                      <td><a href="#" id="edit_subscription_{{preference.pk}}" class="modal-async" data-target='#notification_preference_modals' data-url="{% url 'notifications:edit-subscription' preference.pk %}"><span class="glyphicon glyphicon-pencil" title="{% trans "Edit" %}"></span></a></td>
+                      <td><form id='unsubscribe_{{preference.pk}}' name='delete' action="{% url 'notifications:unsubscribe' preference.pk %}" method='POST'>
+		                    {% csrf_token %}
+		                    <button type='submit' class='btn btn-xs btn-link icon' title="{% trans 'Unsubscribe' %}"><i class='glyphicon glyphicon-remove'></i></button>
+                          </form>
+                      </td>
+                  </tr>
+              {% endfor %}
+            </tbody>
+          </table>
+          <button id="subscribe_notification" class="modal-async btn btn-primary pull-right" type="button" data-target='#notification_preference_modals' data-url="{% url 'notifications:subscribe' %}"><span class="glyphicon glyphicon-plus" title="{% trans "Subscribe" %}"></span>&nbsp;{% trans "Subscribe" %}</button>
+        </div>
+      </div>
+</div>
+<div id='notification_preference_modals'>
+</div>
+<script src="{% static "fir_notifications/notifications.js" %}"></script>
\ No newline at end of file
diff --git a/fir_notifications/templatetags/notifications.py b/fir_notifications/templatetags/notifications.py
index 21de55c9..ebc1cf8c 100644
--- a/fir_notifications/templatetags/notifications.py
+++ b/fir_notifications/templatetags/notifications.py
@@ -22,3 +22,18 @@ def notification_forms(context):
             actions[method_name] = method_object.form(user=context['user'])
     return {'actions': actions}
 
+
+@register.filter
+def display_method(arg):
+    method = registry.methods.get(arg, None)
+    if method is None:
+        return 'Unknown'
+    return method.verbose_name
+
+
+@register.filter
+def display_event(arg):
+    event = registry.events.get(arg, None)
+    if event is None:
+        return 'Unknown'
+    return event.verbose_name
diff --git a/fir_notifications/urls.py b/fir_notifications/urls.py
index f7d8dff9..5f70674f 100644
--- a/fir_notifications/urls.py
+++ b/fir_notifications/urls.py
@@ -4,6 +4,9 @@
 
 
 urlpatterns = [
-    url(r'^preferences$', views.preferences, name='preferences'),
-    url(r'^preferences/(?P<method>[a-zA-Z0-9_]+)$', views.method_configuration, name='method_configuration'),
+    url(r'^subscriptions$', views.subscriptions, name='subscriptions'),
+    url(r'^subscriptions/(?P<object_id>\d+)$', views.edit_subscription, name='edit-subscription'),
+    url(r'^subscriptions/subscribe$', views.edit_subscription, name='subscribe'),
+    url(r'^subscriptions/(?P<object_id>\d+)/unsubscribe$', views.unsubscribe, name='unsubscribe'),
+    url(r'^method/(?P<method>[a-zA-Z0-9_]+)$', views.method_configuration, name='method_configuration'),
 ]
\ No newline at end of file
diff --git a/fir_notifications/views.py b/fir_notifications/views.py
index 70a55105..557c7d80 100644
--- a/fir_notifications/views.py
+++ b/fir_notifications/views.py
@@ -1,16 +1,15 @@
 from django.contrib.auth.decorators import login_required
-from django import forms
 from django.contrib import messages
-from django.shortcuts import redirect, render
-from django.views.decorators.http import require_POST
-from django.contrib.auth import get_user_model
+from django.http import JsonResponse
+from django.shortcuts import redirect, render, get_object_or_404
+from django.template.loader import render_to_string
+from django.views.decorators.http import require_POST, require_GET
+from django.utils.translation import ugettext_lazy as _
 
-from fir_notifications.forms import NotificationPreferenceFormset
+from fir_notifications.forms import NotificationPreferenceForm
 from fir_notifications.models import NotificationPreference
 from fir_notifications.registry import registry
 
-from incidents.models import BusinessLine
-
 
 @require_POST
 @login_required
@@ -27,28 +26,47 @@ def method_configuration(request, method):
     return redirect('user:profile')
 
 
+@require_GET
 @login_required
-def preferences(request):
-
-    class NotificationPreferenceForm(forms.ModelForm):
-        event = forms.ChoiceField(choices=registry.get_event_choices(), disabled=True, widget=forms.HiddenInput())
-        method = forms.ChoiceField(choices=registry.get_method_choices(), disabled=True, widget=forms.HiddenInput())
-        business_lines = forms.ModelMultipleChoiceField(BusinessLine.authorization.for_user(request.user,
-                                                                                            'incidents.view_incidents'),
-                                                        required=False)
+def subscriptions(request):
+    instances = NotificationPreference.objects.filter(user=request.user,
+                                                      event__in=registry.events.keys(),
+                                                      method__in=registry.methods.keys(),
+                                                      business_lines__isnull=False).distinct()
+    return render(request, "fir_notifications/subscriptions.html", {'preferences': instances})
 
-        class Meta:
-            fields = "__all__"
 
-    formset = forms.inlineformset_factory(get_user_model(), NotificationPreference,
-                                          formset=NotificationPreferenceFormset,
-                                          form=NotificationPreferenceForm)
+@login_required
+def edit_subscription(request, object_id=None):
+    instance = None
+    if object_id is not None:
+        instance = get_object_or_404(NotificationPreference, pk=object_id, user=request.user)
     if request.method == 'POST':
-        fs = formset(request.POST, instance=request.user)
-        if fs.is_valid():
-            fs.save()
-        return redirect('user:profile')
+        form = NotificationPreferenceForm(instance=instance, data=request.POST, user=request.user)
+        if form.is_valid():
+            form.save()
+            return JsonResponse({'status': 'success'})
+        else:
+            errors = render_to_string("fir_notifications/subscribe.html",
+                                      {'form': form})
+            return JsonResponse({'status': 'error', 'data': errors})
     else:
-        fs = formset(instance=request.user)
+        form = NotificationPreferenceForm(instance=instance, user=request.user)
+    return render(request, "fir_notifications/subscribe.html", {'form': form})
+
 
-    return render(request, "fir_notifications/preferences.html", {'formset': fs})
+@require_POST
+@login_required
+def unsubscribe(request, object_id=None):
+    if object_id is not None:
+        try:
+            instance = NotificationPreference.objects.get(pk=object_id, user=request.user)
+            instance.delete()
+            messages.info(request, _('Unsubscribed.'))
+        except NotificationPreference.DoesNotExist:
+            messages.error(request, _("Subscription does not exist."))
+        except NotificationPreference.MultipleObjectsReturned:
+            messages.error(request, _("Subscription is invalid."))
+    else:
+        messages.error(request, _("Subscription does not exist."))
+    return redirect('user:profile')

From d55410616a0c679f63aa5a839819faa5c40f9b45 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:12:49 +0100
Subject: [PATCH 59/66] Notifications: Update French translation

---
 .../locale/fr/LC_MESSAGES/django.po           | 76 ++++++++++++++-----
 1 file changed, 56 insertions(+), 20 deletions(-)

diff --git a/fir_notifications/locale/fr/LC_MESSAGES/django.po b/fir_notifications/locale/fr/LC_MESSAGES/django.po
index bb09e748..93152942 100644
--- a/fir_notifications/locale/fr/LC_MESSAGES/django.po
+++ b/fir_notifications/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-01-26 06:51+0100\n"
+"POT-Creation-Date: 2017-01-27 11:04+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -23,7 +23,7 @@ msgctxt "business lines"
 msgid "All"
 msgstr "Toutes"
 
-#: admin.py:20
+#: admin.py:20 forms.py:61 templates/fir_notifications/subscriptions.html:16
 msgid "Business lines"
 msgstr ""
 
@@ -31,11 +31,20 @@ msgstr ""
 msgid "Notifications"
 msgstr ""
 
-#: forms.py:18
+#: forms.py:19
 #, python-format
 msgid "Configure %(method)s"
 msgstr "Configurer %(method)s"
 
+#: forms.py:59 models.py:66 models.py:74 models.py:82 models.py:92
+#: templates/fir_notifications/subscriptions.html:14
+msgid "Event"
+msgstr "Événement"
+
+#: forms.py:60 templates/fir_notifications/subscriptions.html:15
+msgid "Method"
+msgstr "Méthode"
+
 #: methods/jabber.py:28
 msgid "Jabber ID"
 msgstr ""
@@ -100,43 +109,39 @@ msgstr "préférence de notification"
 msgid "notification preferences"
 msgstr "préférences de notification"
 
-#: models.py:64
+#: models.py:65
 msgid "Event created"
 msgstr "Événement créé"
 
-#: models.py:65 models.py:73 models.py:81 models.py:91
-msgid "Event"
-msgstr "Événement"
-
-#: models.py:72
+#: models.py:73
 msgid "Event updated"
 msgstr "Événement mis à jour"
 
-#: models.py:80
+#: models.py:81
 msgid "Event commented"
 msgstr "Événement commenté"
 
-#: models.py:90
+#: models.py:91
 msgid "Event status changed"
 msgstr "Statut de l'événement changé"
 
-#: models.py:98
+#: models.py:99
 msgid "Incident created"
 msgstr "Incident créé"
 
-#: models.py:99 models.py:107 models.py:115 models.py:125
+#: models.py:100 models.py:108 models.py:116 models.py:126
 msgid "Incident"
 msgstr "Incident"
 
-#: models.py:106
+#: models.py:107
 msgid "Incident updated"
 msgstr "Incident mis à jour"
 
-#: models.py:114
+#: models.py:115
 msgid "Incident commented"
 msgstr "Incident commenté"
 
-#: models.py:124
+#: models.py:125
 msgid "Incident status changed"
 msgstr "Statut de l'incident changé"
 
@@ -146,14 +151,45 @@ msgid "Configure %(method_name)s"
 msgstr "Configurer %(method_name)s"
 
 #: templates/fir_notifications/actions_form.html:38
+#: templates/fir_notifications/subscribe.html:36
 msgid "Cancel"
 msgstr "Annuler"
 
 #: templates/fir_notifications/actions_form.html:39
-#: templates/fir_notifications/preferences.html:33
+#: templates/fir_notifications/subscribe.html:37
 msgid "Save"
 msgstr "Enregistrer"
 
-#: templates/fir_notifications/preferences.html:8
-msgid "Notification preferences"
-msgstr "Préférences de notification"
+#: templates/fir_notifications/subscribe.html:9
+msgid "Notification subscription"
+msgstr "Abonnement à une notification"
+
+#: templates/fir_notifications/subscriptions.html:8
+#| msgid "Notification subscription"
+msgid "Notification subscriptions"
+msgstr "Abonnements aux notifications"
+
+#: templates/fir_notifications/subscriptions.html:27
+msgid "Edit"
+msgstr "Éditer"
+
+#: templates/fir_notifications/subscriptions.html:30
+msgid "Unsubscribe"
+msgstr "Se désabonner"
+
+#: templates/fir_notifications/subscriptions.html:37
+msgid "Subscribe"
+msgstr "S'abonner"
+
+#: views.py:65
+msgid "Unsubscribed."
+msgstr "Désabonné."
+
+#: views.py:67 views.py:71
+msgid "Subscription does not exist."
+msgstr "L'abonnement n'existe pas."
+
+#: views.py:69
+msgid "Subscription is invalid."
+msgstr "Abonnement invalide."
+

From 683ddc0d1f9a9ea51363cb4aa7d6810cd302b7c1 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:15:23 +0100
Subject: [PATCH 60/66] Notifications: change notification preferences default
 ordering

---
 .../migrations/0003_auto_20170127_1113.py     | 24 +++++++++++++++++++
 fir_notifications/models.py                   |  1 +
 2 files changed, 25 insertions(+)
 create mode 100644 fir_notifications/migrations/0003_auto_20170127_1113.py

diff --git a/fir_notifications/migrations/0003_auto_20170127_1113.py b/fir_notifications/migrations/0003_auto_20170127_1113.py
new file mode 100644
index 00000000..47210bf8
--- /dev/null
+++ b/fir_notifications/migrations/0003_auto_20170127_1113.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.9 on 2017-01-27 11:13
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('fir_notifications', '0002_user_preference'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='notificationpreference',
+            options={'ordering': ['user', 'event', 'method'], 'verbose_name': 'notification preference', 'verbose_name_plural': 'notification preferences'},
+        ),
+        migrations.AlterField(
+            model_name='methodconfiguration',
+            name='key',
+            field=models.CharField(choices=[(b'email', b'Email'), (b'xmpp', b'XMPP')], max_length=60, verbose_name='method'),
+        ),
+    ]
diff --git a/fir_notifications/models.py b/fir_notifications/models.py
index ff91db8e..d2132e97 100644
--- a/fir_notifications/models.py
+++ b/fir_notifications/models.py
@@ -58,6 +58,7 @@ class Meta:
         verbose_name_plural = _('notification preferences')
         unique_together = (("user", "event", "method"),)
         index_together = ["user", "event", "method"]
+        ordering = ['user', 'event', 'method']
 
 
 if not settings.NOTIFICATIONS_MERGE_INCIDENTS_AND_EVENTS:

From f40fc395560a3962e7a7692493ce886fe53be456 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:17:39 +0100
Subject: [PATCH 61/66] Notifications: sort events and methods choices

---
 fir_notifications/registry.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 77ef9a59..4c423cbe 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -66,10 +66,10 @@ def register_event(self, name, signal, model, callback, verbose_name=None, secti
         signal.connect(callback, sender=model, dispatch_uid="fir_notifications.{}".format(name))
 
     def get_event_choices(self):
-        return [(obj.name, obj.verbose_name) for obj in self.events.values()]
+        return sorted([(obj.name, obj.verbose_name) for obj in self.events.values()])
 
     def get_method_choices(self):
-        return [(obj.name, obj.verbose_name) for obj in self.methods.values()]
+        return sorted([(obj.name, obj.verbose_name) for obj in self.methods.values()])
 
     def get_methods(self):
         return self.methods.values()

From fbfed55264ad8e014869074142ecf811ef13ba18 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:55:32 +0100
Subject: [PATCH 62/66] Notifications: Show event section in event choices

---
 fir_notifications/registry.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/fir_notifications/registry.py b/fir_notifications/registry.py
index 4c423cbe..ec0433e3 100644
--- a/fir_notifications/registry.py
+++ b/fir_notifications/registry.py
@@ -10,7 +10,7 @@
 
 @python_2_unicode_compatible
 class RegisteredEvent(object):
-    def __init__(self, name, model, verbose_name=None, section = None):
+    def __init__(self, name, model, verbose_name=None, section=None):
         self.name = name
         if section is None:
             section = apps.get_app_config(model._meta.app_label).verbose_name
@@ -66,7 +66,12 @@ def register_event(self, name, signal, model, callback, verbose_name=None, secti
         signal.connect(callback, sender=model, dispatch_uid="fir_notifications.{}".format(name))
 
     def get_event_choices(self):
-        return sorted([(obj.name, obj.verbose_name) for obj in self.events.values()])
+        results = OrderedDict()
+        for obj in self.events.values():
+            if obj.section not in results:
+                results[obj.section] = list()
+            results[obj.section].append((obj.name, obj.verbose_name))
+        return [(section, sorted(choices)) for section, choices in results.items()]
 
     def get_method_choices(self):
         return sorted([(obj.name, obj.verbose_name) for obj in self.methods.values()])

From 9c7487825514190a81c3e6cd6ee19def1d55aa2f Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Fri, 27 Jan 2017 11:56:00 +0100
Subject: [PATCH 63/66] Notifications: Show event section in user preferences

---
 .../templates/fir_notifications/subscriptions.html        | 2 ++
 fir_notifications/templatetags/notifications.py           | 8 ++++++++
 2 files changed, 10 insertions(+)

diff --git a/fir_notifications/templates/fir_notifications/subscriptions.html b/fir_notifications/templates/fir_notifications/subscriptions.html
index 38c77450..263d666e 100644
--- a/fir_notifications/templates/fir_notifications/subscriptions.html
+++ b/fir_notifications/templates/fir_notifications/subscriptions.html
@@ -11,6 +11,7 @@ <h3 class="panel-title">{% trans "Notification subscriptions" %}</h3>
           <table class="table table-condensed">
             <thead>
                 <tr>
+                    <th>{%  trans "Section" %}</th>
                     <th>{%  trans "Event" %}</th>
                     <th>{%  trans "Method" %}</th>
                     <th>{%  trans "Business lines" %}</th>
@@ -21,6 +22,7 @@ <h3 class="panel-title">{% trans "Notification subscriptions" %}</h3>
             <tbody>
               {% for preference in preferences %}
                   <tr>
+                    <td>{{ preference.event|display_event_section }}</td>
                     <td>{{ preference.event|display_event }}</td>
                     <td>{{ preference.method|display_method }}</td>
                     <td>{{ preference.business_lines.all|join:', ' }}</td>
diff --git a/fir_notifications/templatetags/notifications.py b/fir_notifications/templatetags/notifications.py
index ebc1cf8c..ad987639 100644
--- a/fir_notifications/templatetags/notifications.py
+++ b/fir_notifications/templatetags/notifications.py
@@ -37,3 +37,11 @@ def display_event(arg):
     if event is None:
         return 'Unknown'
     return event.verbose_name
+
+
+@register.filter
+def display_event_section(arg):
+    event = registry.events.get(arg, None)
+    if event is None:
+        return 'Unknown'
+    return event.section

From 326a029210afc498993d3f91b6a4ee23aa66cb0b Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 30 Jan 2017 12:25:33 +0100
Subject: [PATCH 64/66] Notifications: XMPP notifications don't need a running
 XMPP server on start

---
 fir_notifications/methods/jabber.py | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/fir_notifications/methods/jabber.py b/fir_notifications/methods/jabber.py
index f41de560..f2d26524 100644
--- a/fir_notifications/methods/jabber.py
+++ b/fir_notifications/methods/jabber.py
@@ -45,17 +45,23 @@ def __init__(self):
             self.connection_tuple = (self.server, self.port)
             self.use_srv = False
         self.client = Client(self.jid.getDomain())
-        if not self.client.connect(server=self.connection_tuple, use_srv=self.use_srv):
-            self.server_configured = False
-            return
-        if not self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource()):
-            self.server_configured = False
-            return
-        self.client.disconnected()
+        if self.client.connect(server=self.connection_tuple, use_srv=self.use_srv):
+            self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource())
+            self.client.disconnected()
         self.server_configured = True
 
+    def _ensure_connection(self):
+        if not hasattr(self.client, 'Dispatcher'):
+            if self.client.connect(server=self.connection_tuple, use_srv=self.use_srv):
+                if self.client.auth(self.jid.getNode(), self.password, resource=self.jid.getResource()):
+                    return True
+            return False
+        return self.client.reconnectAndReauth()
+
     def send(self, event, users, instance, paths):
-        self.client.reconnectAndReauth()
+        if not self._ensure_connection():
+            print("Cannot contact the XMPP server")
+            return
         for user, templates in users.items():
             jid = self._get_jid(user)
             if not self.enabled(event, user, paths) or jid is None:

From ca0ab370e7605d8eabd242337171fe7ee6ebcd49 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 30 Jan 2017 12:26:26 +0100
Subject: [PATCH 65/66] Notifications: Fix missing user field in preference
 form

---
 fir_notifications/forms.py | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/fir_notifications/forms.py b/fir_notifications/forms.py
index df343f2e..959219f0 100644
--- a/fir_notifications/forms.py
+++ b/fir_notifications/forms.py
@@ -2,6 +2,7 @@
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth import get_user_model
 
 from incidents.models import BusinessLine
 
@@ -56,10 +57,16 @@ def __init__(self, *args, **kwargs):
             self.fields['event'].disabled = True
             self.fields['method'].disabled = True
 
+    def clean_user(self):
+        if self.user is None:
+            raise forms.ValidationError(_("Notification preference must be linked to a user."))
+        return self.user
+
+    user = forms.ModelChoiceField(queryset=get_user_model().objects.all(), widget=forms.HiddenInput(), required=False)
     event = forms.ChoiceField(choices=registry.get_event_choices(), label=_('Event'))
     method = forms.ChoiceField(choices=registry.get_method_choices(), label=_('Method'))
     business_lines = forms.ModelMultipleChoiceField(BusinessLine.objects.all(), label=_('Business lines'))
 
     class Meta:
-        exclude = ('user', )
+        fields = '__all__'
         model = NotificationPreference

From b16e68d261f6bc8c4ce3baad20e0616dd29af853 Mon Sep 17 00:00:00 2001
From: Gaetan Crahay <gaetan@crahay.eu>
Date: Mon, 30 Jan 2017 12:29:22 +0100
Subject: [PATCH 66/66] Notifications: update French translation

---
 .../locale/fr/LC_MESSAGES/django.po           | 33 ++++++++++++-------
 1 file changed, 22 insertions(+), 11 deletions(-)

diff --git a/fir_notifications/locale/fr/LC_MESSAGES/django.po b/fir_notifications/locale/fr/LC_MESSAGES/django.po
index 93152942..552fe344 100644
--- a/fir_notifications/locale/fr/LC_MESSAGES/django.po
+++ b/fir_notifications/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-01-27 11:04+0100\n"
+"POT-Creation-Date: 2017-01-30 12:26+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -23,7 +23,7 @@ msgctxt "business lines"
 msgid "All"
 msgstr "Toutes"
 
-#: admin.py:20 forms.py:61 templates/fir_notifications/subscriptions.html:16
+#: admin.py:20 forms.py:68 templates/fir_notifications/subscriptions.html:17
 msgid "Business lines"
 msgstr ""
 
@@ -31,17 +31,21 @@ msgstr ""
 msgid "Notifications"
 msgstr ""
 
-#: forms.py:19
+#: forms.py:20
 #, python-format
 msgid "Configure %(method)s"
 msgstr "Configurer %(method)s"
 
-#: forms.py:59 models.py:66 models.py:74 models.py:82 models.py:92
-#: templates/fir_notifications/subscriptions.html:14
+#: forms.py:62
+msgid "Notification preference must be linked to a user."
+msgstr "La préférence de notification doit être reliée à un utilisateur."
+
+#: forms.py:66 models.py:66 models.py:74 models.py:82 models.py:92
+#: templates/fir_notifications/subscriptions.html:15
 msgid "Event"
 msgstr "Événement"
 
-#: forms.py:60 templates/fir_notifications/subscriptions.html:15
+#: forms.py:67 templates/fir_notifications/subscriptions.html:16
 msgid "Method"
 msgstr "Méthode"
 
@@ -156,28 +160,36 @@ msgid "Cancel"
 msgstr "Annuler"
 
 #: templates/fir_notifications/actions_form.html:39
+#: templates/fir_notifications/preferences.html:33
 #: templates/fir_notifications/subscribe.html:37
 msgid "Save"
 msgstr "Enregistrer"
 
+#: templates/fir_notifications/preferences.html:8
+msgid "Notification preferences"
+msgstr "Préférences de notification"
+
 #: templates/fir_notifications/subscribe.html:9
 msgid "Notification subscription"
 msgstr "Abonnement à une notification"
 
 #: templates/fir_notifications/subscriptions.html:8
-#| msgid "Notification subscription"
 msgid "Notification subscriptions"
 msgstr "Abonnements aux notifications"
 
-#: templates/fir_notifications/subscriptions.html:27
+#: templates/fir_notifications/subscriptions.html:14
+msgid "Section"
+msgstr ""
+
+#: templates/fir_notifications/subscriptions.html:29
 msgid "Edit"
 msgstr "Éditer"
 
-#: templates/fir_notifications/subscriptions.html:30
+#: templates/fir_notifications/subscriptions.html:32
 msgid "Unsubscribe"
 msgstr "Se désabonner"
 
-#: templates/fir_notifications/subscriptions.html:37
+#: templates/fir_notifications/subscriptions.html:39
 msgid "Subscribe"
 msgstr "S'abonner"
 
@@ -192,4 +204,3 @@ msgstr "L'abonnement n'existe pas."
 #: views.py:69
 msgid "Subscription is invalid."
 msgstr "Abonnement invalide."
-