diff --git a/README.md b/README.md index 7c9783881f5e67288e4029d9b71486d791777806..8833197a804759caa48147c777f0acfc1e412236 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ An object allowing a User to be notified of any change on a resource or a contai | `object` | `URLField` | | ID of the resource or the container to watch | | `inbox` | `URLField` | | ID of the inbox to notify when the resource or the container change | | `field` | `CharField` | | (optional) if set, then object['field'] will be sent in the notification, not object | +| `disable_automatic_notifications` | `BooleanField` | `False` | By default, notifications will be sent to this inbox everytime the target object/container is updated. Setting this flag to true prevents this behaviour, meaning that notifications will have to be triggered manually | For convenience, when you create a subscription on an object, DjangoLDP-Notification will parse the object for any one-to-many nested field relations. It will then create nested-subscriptions, i.e. a subscription on the nested field which sends an update to the same inbox, passing the parent model. If this behaviour is undesired you can delete the `Subscription` instance diff --git a/djangoldp_notification/migrations/0013_auto_20211016_1203.py b/djangoldp_notification/migrations/0013_auto_20211016_1203.py new file mode 100644 index 0000000000000000000000000000000000000000..dff381386b73b675738a80565ccfeb3bb093bc48 --- /dev/null +++ b/djangoldp_notification/migrations/0013_auto_20211016_1203.py @@ -0,0 +1,51 @@ +# Generated by Django 2.2.23 on 2021-10-16 10:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import djangoldp.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_notification', '0012_auto_20210729_1912'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='author', + field=djangoldp.fields.LDPUrlField(help_text='the sender of the notification'), + ), + migrations.AlterField( + model_name='notification', + name='object', + field=djangoldp.fields.LDPUrlField(help_text='the urlid of the saved object being transmitted by the notification'), + ), + migrations.AlterField( + model_name='notification', + name='unread', + field=models.BooleanField(default=True, help_text='set to False after the user has seen the notification'), + ), + migrations.AlterField( + model_name='notification', + name='user', + field=models.ForeignKey(help_text='the recipient of the notification', on_delete=django.db.models.deletion.CASCADE, related_name='inbox', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='notificationsetting', + name='receiveMail', + field=models.BooleanField(default=True, help_text='if set to True the user will receive an email on notification receipt'), + ), + migrations.AlterField( + model_name='subscription', + name='inbox', + field=models.URLField(help_text='the inbox of the recipient of the notification'), + ), + migrations.AlterField( + model_name='subscription', + name='object', + field=models.URLField(help_text='the urlid of the object being subscribed'), + ), + ] diff --git a/djangoldp_notification/migrations/0014_subscription_disable_automatic_notifications.py b/djangoldp_notification/migrations/0014_subscription_disable_automatic_notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..dd0d10376732aec05473b17e18d04961954238bf --- /dev/null +++ b/djangoldp_notification/migrations/0014_subscription_disable_automatic_notifications.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.23 on 2021-10-16 10:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_notification', '0013_auto_20211016_1203'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='disable_automatic_notifications', + field=models.BooleanField(default=False, help_text='By default, notifications will be sent to this inbox everytime the target object/container is updated. Setting this flag to true prevents this behaviour, meaning that notifications will have to be triggered manually'), + ), + ] diff --git a/djangoldp_notification/models.py b/djangoldp_notification/models.py index 7650b999f35c4229378bee7c1edb5897f34c96af..e41508a5abbed6187b747bde85b9dd88a93c87ac 100644 --- a/djangoldp_notification/models.py +++ b/djangoldp_notification/models.py @@ -23,13 +23,13 @@ logger = logging.getLogger('djangoldp') class Notification(Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='inbox', on_delete=models.deletion.CASCADE) - author = LDPUrlField() - object = LDPUrlField() + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='inbox', on_delete=models.deletion.CASCADE, help_text='the recipient of the notification') + author = LDPUrlField(help_text='the sender of the notification') + object = LDPUrlField(help_text='the urlid of the saved object being transmitted by the notification') type = models.CharField(max_length=255) summary = models.TextField(blank=True, default='') date = models.DateTimeField(auto_now_add=True) - unread = models.BooleanField(default=True) + unread = models.BooleanField(default=True, help_text='set to False after the user has seen the notification') class Meta(Model.Meta): owner_field = 'user' @@ -76,7 +76,7 @@ class Notification(Model): class NotificationSetting(Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="settings") - receiveMail = models.BooleanField(default=True) + receiveMail = models.BooleanField(default=True, help_text='if set to True the user will receive an email on notification receipt') class Meta: auto_author = 'user' @@ -92,10 +92,12 @@ class NotificationSetting(Model): return '{} ({})'.format(self.user.get_full_name(), self.user.urlid) class Subscription(Model): - object = models.URLField() - inbox = models.URLField() + object = models.URLField(help_text='the urlid of the object being subscribed') + inbox = models.URLField(help_text='the inbox of the recipient of the notification') field = models.CharField(max_length=255, blank=True, null=True, help_text='if set to a field name on the object model, the field will be passed instead of the object instance') + disable_automatic_notifications = models.BooleanField(default=False, + help_text='By default, notifications will be sent to this inbox everytime the target object/container is updated. Setting this flag to true prevents this behaviour, meaning that notifications will have to be triggered manually') def __str__(self): return '{}'.format(self.object) @@ -165,7 +167,7 @@ def send_notification(sender, instance, **kwargs): return # dispatch a notification for every Subscription on this resource - for subscription in Subscription.objects.filter(models.Q(object=url_resource) | models.Q(object=url_container)): + 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: @@ -222,13 +224,33 @@ def send_request(target, object_iri, instance, created): ActivityQueueService.send_activity(target, json) +def get_default_email_sender_djangoldp_instance(): + ''' + :return: the configured email host if it can find one, or None + ''' + email_from = (getattr(settings, 'DEFAULT_FROM_EMAIL', False) or getattr(settings, 'EMAIL_HOST_USER', False)) + + if not email_from: + jabber_host = getattr(settings, 'JABBER_DEFAULT_HOST', False) + + if jabber_host: + return "noreply@" + jabber_host + return None + + return email_from + @receiver(post_save, sender=Notification) def send_email_on_notification(sender, instance, created, **kwargs): if created \ and instance.summary \ - and getattr(settings,'JABBER_DEFAULT_HOST',False) \ and instance.user.email \ and instance.type in ('Message', 'Mention'): + + email_from = get_default_email_sender_djangoldp_instance() + + if email_from is None or not instance.user.settings.receiveMail: + return + # get author name, and store in who try: # local author @@ -269,15 +291,14 @@ def send_email_on_notification(sender, instance, created, **kwargs): } ) - if instance.user.settings.receiveMail: - send_mail( - 'Notification sur ' + on, - instance.summary, - (getattr(settings, 'EMAIL_HOST_USER', False) or "noreply@" + settings.JABBER_DEFAULT_HOST), - [instance.user.email], - fail_silently=True, - html_message=html_message - ) + send_mail( + 'Notification sur ' + on, + instance.summary, + email_from, + [instance.user.email], + fail_silently=True, + html_message=html_message + ) @receiver(post_save, sender=settings.AUTH_USER_MODEL) def create_user_settings(sender, instance, created, **kwargs): diff --git a/djangoldp_notification/tests/test_models.py b/djangoldp_notification/tests/test_models.py index c8c64877154b338487476a459728faf08a904389..336e21d9be2b903ae112f2651ea3da0030f63d17 100644 --- a/djangoldp_notification/tests/test_models.py +++ b/djangoldp_notification/tests/test_models.py @@ -5,7 +5,7 @@ from djangoldp_account.models import LDPUser from djangoldp_notification.models import Subscription -class TestSubscription(APITestCase): +class TestModel(APITestCase): circle_user1_url = "http://localhost:8000/circles/1/" circle_user2_url = "http://localhost:8000/circles/2/" diff --git a/djangoldp_notification/views.py b/djangoldp_notification/views.py index 5df49fc429799444affe024a2df9fd4e8c641af3..b382921569225a2f9d6dfebe5439c04bef0d677a 100644 --- a/djangoldp_notification/views.py +++ b/djangoldp_notification/views.py @@ -1,12 +1,52 @@ +from django.conf import settings +from django.contrib.auth import get_user_model +from django.http import Http404 +from rest_framework import status +from rest_framework.response import Response + +from djangoldp.models import Model from djangoldp.serializers import LDPSerializer from djangoldp.views import LDPViewSet from djangoldp.pagination import LDPPagination +import logging + + +logger = logging.getLogger('djangoldp') class LDPNotificationsPagination(LDPPagination): default_limit = 80 +def filter_object_is_permitted(recipient, data): + ''' + applies filter on passed object data + returns True if the object is permitted, False if not + ''' + obj = data['object'] + + # if a str (urlid) is given then no filtering is to be applied + if isinstance(obj, str): + return True + + # the type must be given for a filter to be resolved successfully + if not '@type' in obj or obj['@type'] is None: + logger.error('djangoldp_notification filter ERR in object serialization. received ' + str(obj) + ' without serialized type to identify model. Please include the @type in your serialization') + return False + + object_model = Model.get_subclass_with_rdf_type(obj['@type']) + + if object_model is None: + logger.error('djangoldp_notification filter ERR in object serialization. Cannot resolve type given ' + str(obj['type'])) + return False + + if not hasattr(object_model, 'permit_notification'): + logger.error('djangoldp_notification filter ERR. Resolved type ' + str(obj['@type']) + ' but this Model did not have the required function permit_notification defined on it') + return False + + return object_model.permit_notification(recipient, data) + + class LDPNotificationsViewSet(LDPViewSet): '''overridden LDPViewSet to force pagination''' pagination_class = LDPNotificationsPagination diff --git a/setup.cfg b/setup.cfg index 3046ee8095afe966b4ae9c88227020c29ea9943d..0990480bf1f5dd82b1374fc4f881788a01b68e20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ license = MIT packages = find: install_requires = djangoldp~=2.1.0 - djangoldp_account~=2.1 + djangoldp_account>=2.1 [options.extras_require] include_package_data = True