Mise en œuvre d'un système d'alertes utilisateur avec Django

Prérequis et installation

Assurez-vous que Python et pip sont installés sur votre système. Installez ensuite Django et les packages supplémentaires nécessaires.

pip install django celery redis django-bootstrap4

Structure de l'application

Créez le projet Django et une application dédiée aux alertes.

django-admin startproject mon_projet
cd mon_projet
python manage.py startapp alertes

Modèle de données pour les alertes

Définisons un modèle pour stocker les notifications destinées aux utilisateurs.

# alertes/models.py
from django.db import models
from django.contrib.auth import get_user_model

Utilisateur = get_user_model()

class Alerte(models.Model):
    destinataire = models.ForeignKey(Utilisateur, on_delete=models.CASCADE, related_name='alertes_recues')
    contenu = models.TextField()
    date_creation = models.DateTimeField(auto_now_add=True)
    est_consultee = models.BooleanField(default=False)
    type_alerte = models.CharField(max_length=50, choices=[('info', 'Information'), ('action', 'Action requise'), ('erreur', 'Erreur')], default='info')
    
    class Meta:
        ordering = ['-date_creation']
        verbose_name = 'Alerte utilisateur'
        verbose_name_plural = 'Alertes utilisateurs'
    
    def __str__(self):
        return f"Alerte {self.pk} pour {self.destinataire.username}"

Configuration des signaux Django

Nous utiliserons les signaux Django pour déclencher la création d'alertes lors d'événements spécifiques.

# alertes/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Alerte

@receiver(post_save, sender='auth.User')
def creer_alerte_bienvenue(sender, instance, created, **kwargs):
    """Crée une alerte de bienvenue pour chaque nouvel utilisateur inscrit."""
    if created:
        Alerte.objects.create(
            destinataire=instance,
            contenu=f"Bienvenue {instance.username} ! Votre compte a été créé avec succès.",
            type_alerte='info'
        )

Vues pour la gestion des alertes

Implémentez les vues pour afficher, marquer comme lues et supprimer les alertes.

# alertes/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from .models import Alerte

@login_required
def liste_alertes(request):
    """Affiche la liste des alertes de l'utilisateur connecté."""
    alertes_utilisateur = Alerte.objects.filter(destinataire=request.user)
    nb_non_lues = alertes_utilisateur.filter(est_consultee=False).count()
    contexte = {
        'alertes': alertes_utilisateur,
        'compteur_non_lues': nb_non_lues,
        'est_page_liste': True
    }
    return render(request, 'alertes/liste.html', contexte)

@login_required
def marquer_comme_lue(request, alerte_id):
    """Marque une alerte spécifique comme lue."""
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        alerte = get_object_or_404(Alerte, pk=alerte_id, destinataire=request.user)
        alerte.est_consultee = True
        alerte.save()
        nouveau_compteur = Alerte.objects.filter(destinataire=request.user, est_consultee=False).count()
        return JsonResponse({'succes': True, 'nouveau_compteur': nouveau_compteur})
    return JsonResponse({'succes': False}, status=400)

@login_required
def supprimer_alerte(request, alerte_id):
    """Supprime une alerte."""
    alerte = get_object_or_404(Alerte, pk=alerte_id, destinataire=request.user)
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        alerte.delete()
        nouveau_compteur = Alerte.objects.filter(destinataire=request.user, est_consultee=False).count()
        return JsonResponse({'succes': True, 'nouveau_compteur': nouveau_compteur})
    alerte.delete()
    return redirect('liste_alertes')

@login_required
def compteur_alertes(request):
    """Retourne le nombre d'alertes non lues (pour appel AJAX)."""
    nombre = Alerte.objects.filter(destinataire=request.user, est_consultee=False).count()
    return JsonResponse({'compteur': nombre})

Configuration des URLs

Définissez les routes correspondant aux vues des alertes.

# alertes/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.liste_alertes, name='liste_alertes'),
    path('marquer-lue/<int:alerte_id>/', views.marquer_comme_lue, name='marquer_comme_lue'),
    path('supprimer/<int:alerte_id>/', views.supprimer_alerte, name='supprimer_alerte'),
    path('compteur/', views.compteur_alertes, name='compteur_alertes'),
]

N'incluez ces URLs dans le fichier principal du projet.

# mon_projet/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('mes-alertes/', include('alertes.urls')),
    # ... autres URLs ...
]

Gabarits HTML avec Bootstrap

Créez le modèle d'affichage principal pour les alertes, intégrant Bootstrap pour la présentation.

<!-- alertes/templates/alertes/liste.html -->
{% load static %}

<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mes Alertes</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h1>Centre de Notifications</h1>
            <span class="badge bg-primary rounded-pill" id="badge-compteur">
                {{ compteur_non_lues }}
            </span>
        </div>
        
        <div class="row">
            <div class="col-md-3">
                <div class="list-group">
                    <a href="#" class="list-group-item list-group-item-action active">Toutes les alertes</a>
                    <a href="#" class="list-group-item list-group-item-action">Non lues ({{ compteur_non_lues }})</a>
                </div>
            </div>
            
            <div class="col-md-9">
                <div class="list-group">
                    {% for alerte in alertes %}
                    <div class="list-group-item list-group-item-action d-flex justify-content-between align-items-start" 
                         id="alerte-{{ alerte.pk }}" 
                         data-id="{{ alerte.pk }}">
                        <div class="ms-2 me-auto">
                            <div class="fw-bold {{ alerte.est_consultee|yesno:'text-muted,' }}">
                                {{ alerte.get_type_alerte_display }}
                            </div>
                            {{ alerte.contenu }}
                            <small class="text-muted d-block mt-1">{{ alerte.date_creation|timesince }}</small>
                        </div>
                        <div class="btn-group btn-group-sm">
                            {% if not alerte.est_consultee %}
                            <button type="button" class="btn btn-outline-primary btn-marquer-lue" 
                                    data-url="{% url 'marquer_comme_lue' alerte.pk %}">
                                Lue
                            </button>
                            {% endif %}
                            <button type="button" class="btn btn-outline-danger btn-supprimer"
                                    data-url="{% url 'supprimer_alerte' alerte.pk %}">
                                &times;
                            </button>
                        </div>
                    </div>
                    {% empty %}
                    <div class="list-group-item text-center py-4">
                        <p class="text-muted mb-0">Aucune alerte pour le moment.</p>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script>
        $(document).ready(function() {
            function rafraichirCompteur() {
                $.get("{% url 'compteur_alertes' %}", function(data) {
                    $('#badge-compteur').text(data.compteur);
                });
            }
            
            setInterval(rafraichirCompteur, 30000);
            
            $('.btn-marquer-lue').click(function() {
                var btn = $(this);
                var url = btn.data('url');
                $.post(url, {
                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                }, function(data) {
                    if (data.succes) {
                        btn.closest('.list-group-item').find('.fw-bold').addClass('text-muted');
                        btn.fadeOut(300, function() { $(this).remove(); });
                        $('#badge-compteur').text(data.nouveau_compteur);
                    }
                });
            });
            
            $('.btn-supprimer').click(function() {
                if (confirm("Supprimer cette alerte définitivement ?")) {
                    var btn = $(this);
                    var url = btn.data('url');
                    var element = btn.closest('.list-group-item');
                    $.post(url, {
                        'csrfmiddlewaretoken': '{{ csrf_token }}'
                    }, function(data) {
                        if (data.succes) {
                            element.slideUp(300, function() { $(this).remove(); });
                            $('#badge-compteur').text(data.nouveau_compteur);
                        }
                    });
                }
            });
        });
    </script>
</body>
</html>

Traitement asynchrone avec Celery

Pour les opérations coûteuses, configurez Celery avec Redis comme broker.

# mon_projet/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mon_projet.settings')

application = Celery('mon_projet')
application.config_from_object('django.conf:settings', namespace='CELERY')
application.autodiscover_tasks()

Ajoutez la configuration dans settings.py :

# Configuration Celery dans settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'

Définissez une tâche asynchrone pour générer des alertes en arrière-plan.

# alertes/tasks.py
from celery import shared_task
from .models import Alerte
from django.contrib.auth import get_user_model

Utilisateur = get_user_model()

@shared_task
def envoyer_alerte_masse(type_alerte, contenu, liste_utilisateurs_ids):
    """Envoie une alerte à une liste d'utilisateurs."""
    utilisateurs = Utilisateur.objects.filter(pk__in=liste_utilisateurs_ids)
    alertes_a_creer = []
    for user in utilisateurs:
        alertes_a_creer.append(
            Alerte(destinataire=user, contenu=contenu, type_alerte=type_alerte)
        )
    Alerte.objects.bulk_create(alertes_a_creer)
    return f"{len(alertes_a_creer)} alertes envoyées."

@shared_task
def generer_rapport_alertes(user_id):
    """Génère un rapport résumé des alertes pour un utilisateur."""
    utilisateur = Utilisateur.objects.get(pk=user_id)
    total = Alerte.objects.filter(destinataire=utilisateur).count()
    non_lues = Alerte.objects.filter(destinataire=utilisateur, est_consultee=False).count()
    # ... logique de génération de rapport ...
    return f"Rapport généré pour {utilisateur.username}. {non_lues} alertes non lues."

Déclenchez ces tâches depuis vos vues ou autres services.

# Exemple d'utilisation dans une vue
from .tasks import envoyer_alerte_masse, generer_rapport_alertes

def vue_admin_alertes(request):
    if request.method == 'POST' and request.user.is_staff:
        type_alerte = request.POST.get('type')
        contenu = request.POST.get('contenu')
        tous_utilisateurs = list(Utilisateur.objects.values_list('id', flat=True))
        
        # Exécution asynchrone
        envoyer_alerte_masse.delay(type_alerte, contenu, tous_utilisateurs)
        
        return redirect('tableau_bord_admin')
    # ... reste de la vue ...

Configuration du projet principal

Assurez-vous que l'application alertes est enregistrée dans INSTALLED_APPS et que les signaux sont chargés.

# alertes/apps.py
from django.apps import AppConfig

class AlertesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'alertes'
    verbose_name = "Système d'alertes"

    def ready(self):
        import alertes.signals  # Charge les signaux
# Dans settings.py
INSTALLED_APPS = [
    ...
    'alertes.apps.AlertesConfig',
    'bootstrap4',
    ...
]

Étiquettes: Django signaux Django notifications utilisateur Bootstrap Celery

Publié le 11 juillet à 17h27