From 7b80ce179f6ee9091e858fc2c0840d263e260c64 Mon Sep 17 00:00:00 2001
From: Sylvain Le Bon <sylvain@startinblox.com>
Date: Thu, 12 Oct 2023 10:55:56 +0200
Subject: [PATCH] feature: notification on m2m change

---
 djangoldp_notification/models.py | 105 ++++++++++++++++---------------
 1 file changed, 54 insertions(+), 51 deletions(-)

diff --git a/djangoldp_notification/models.py b/djangoldp_notification/models.py
index 5e70105..bd43a28 100644
--- a/djangoldp_notification/models.py
+++ b/djangoldp_notification/models.py
@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
 from django.core.mail import send_mail
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
-from django.db.models.signals import post_save, post_delete
+from django.db.models.signals import post_save, post_delete, m2m_changed
 from django.dispatch import receiver
 from django.template import loader
 from django.urls import NoReverseMatch, get_resolver, reverse
@@ -153,63 +153,55 @@ def create_nested_subscribers(sender, instance, created, **kwargs):
 # --- SUBSCRIPTION SYSTEM ---
 @receiver(post_save, dispatch_uid="callback_notif")
 @receiver(post_delete, dispatch_uid="delete_callback_notif")
-def send_notification(sender, instance, **kwargs):
-    if(type(instance).__name__ != "ScheduledActivity" and type(instance).__name__ != "LogEntry" and type(instance).__name__ != "Activity"):
-        if sender != Notification:
-            # don't send notifications for foreign resources
-            if hasattr(instance, 'urlid') and Model.is_external(instance.urlid):
-                return
-
-            recipients = []
-            try:
-                url_container = settings.BASE_URL + Model.container_id(instance)
-                url_resource = settings.BASE_URL + Model.resource_id(instance)
-            except NoReverseMatch:
-                return
-
-            # dispatch a notification for every Subscription on this resource
-            for subscription in Subscription.objects.filter(models.Q(disable_automatic_notifications=False) & (models.Q(object=url_resource) | models.Q(object=url_container))):
-                if subscription.inbox not in recipients and (not subscription.is_backlink or not kwargs.get("created")):
-                    # I may have configured to send the subscription to a foreign key
-                    if subscription.field is not None and len(subscription.field) > 1:
-                        try:
-                            if kwargs.get("created"):
-                                continue
+@receiver(m2m_changed, dispatch_uid="m2m_callback_notif")
+def notify(sender, instance, created=None, model=None, pk_set=set(), action='', **kwargs):
+    if type(instance).__name__ not in ["ScheduledActivity", "LogEntry", "Activity"] and sender != Notification \
+        and action not in ['pre_add', 'pre_remove']:
+        if action or created is False:
+            request_type = 'update' #M2M change or post_save
+        elif created:
+            request_type = 'creation'
+        else:
+            request_type = "deletion"
+        send_notifications(instance, request_type)
+        if model and pk_set:
+            # Notify the reverse relations
+            send_notifications(model.objects.get(id=pk_set.pop()), 'update')
 
-                            instance = getattr(instance, subscription.field, instance)
 
-                            # don't send notifications for foreign resources
-                            if hasattr(instance, 'urlid') and Model.is_external(instance.urlid):
-                                continue
+def send_notifications(instance, request_type):
+    try:
+        url_container = settings.BASE_URL + Model.container_id(instance)
+        url_resource = settings.BASE_URL + Model.resource_id(instance)
+    except NoReverseMatch:
+        return
+    recipients = []
+    # don't send notifications for foreign resources
+    if hasattr(instance, 'urlid') and Model.is_external(instance.urlid):
+        return
+    # dispatch a notification for every Subscription on this resource
+    for subscription in Subscription.objects.filter(models.Q(disable_automatic_notifications=False) & (models.Q(object=url_resource) | models.Q(object=url_container))):
+        if subscription.inbox not in recipients and (not subscription.is_backlink or request_type != 'creation'):
+            # I may have configured to send the subscription to a foreign key
+            if subscription.field is not None and len(subscription.field) > 1 and request_type != 'creation':
+                try:
+                    instance = getattr(instance, subscription.field, instance)
+                    # don't send notifications for foreign resources
+                    if hasattr(instance, 'urlid') and Model.is_external(instance.urlid):
+                        continue
 
-                            url_resource = settings.BASE_URL + Model.resource_id(instance)
-                        except NoReverseMatch:
-                            continue
-                        except ObjectDoesNotExist:
-                            continue
+                    url_resource = settings.BASE_URL + Model.resource_id(instance)
+                except NoReverseMatch:
+                    continue
+                except ObjectDoesNotExist:
+                    continue
 
-                    send_request(subscription.inbox, url_resource, instance, kwargs.get("created"))
-                    recipients.append(subscription.inbox)
+            send_request(subscription.inbox, url_resource, instance, request_type)
+            recipients.append(subscription.inbox)
 
 
-@receiver(activity_sending_finished, sender=ActivityQueueService)
-def _handle_prosody_response(sender, response, saved_activity, **kwargs):
-    '''callback function for handling a response from Prosody on a notification'''
-    # if text is defined in the response body then it's an error
-    if saved_activity is not None:
-        response_body = saved_activity.response_to_json()
-        if 'condition' in response_body:
-            logger.error("[DjangoLDP-Notification.models._handle_prosody_response] error in Prosody response " +
-                         str(response_body))
-
-
-def send_request(target, object_iri, instance, created):
+def send_request(target, object_iri, instance, request_type):
     author = getattr(getattr(instance, MODEL_MODIFICATION_USER_FIELD, None), "urlid", str(_("Auteur inconnu")))
-    if(created is not None):
-        request_type = "creation" if created else "update"
-    else:
-        request_type = "deletion"
-
     # local inbox
     if target.startswith(settings.SITE_URL):
         user = Model.resolve_parent(target.replace(settings.SITE_URL, ''))
@@ -225,6 +217,17 @@ def send_request(target, object_iri, instance, created):
         ActivityQueueService.send_activity(target, json)
 
 
+@receiver(activity_sending_finished, sender=ActivityQueueService)
+def _handle_prosody_response(sender, response, saved_activity, **kwargs):
+    '''callback function for handling a response from Prosody on a notification'''
+    # if text is defined in the response body then it's an error
+    if saved_activity is not None:
+        response_body = saved_activity.response_to_json()
+        if 'condition' in response_body:
+            logger.error("[DjangoLDP-Notification.models._handle_prosody_response] error in Prosody response " +
+                         str(response_body))
+
+
 def get_default_email_sender_djangoldp_instance():
     '''
     :return: the configured email host if it can find one, or None
-- 
GitLab