diff --git a/README.md b/README.md index da496466539a2d8cf73e6f49f8c264eee142b57b..0a82674b82bff1dbecbdf04bbbc1c9c42555b9b8 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,13 @@ You can automatically create required subscriptions based on your settings.py wi # Middlewares -There is a `CurrentUserMiddleware` that catches the connected user of the last performed HTTP request and adds -to every model before it is saved. This is useful if you need to get the connected user that performed -the last HTTP request in a `pre_saved` signal. You can get it by using the following line : +There is a `CurrentUserMiddleware` that catches the connected user of the current HTTP request and makes it available through the `get_current_user` function : ```python -getattr(instance, MODEL_MODIFICATION_USER_FIELD, "Unknown user") +from djangoldp_notification.middlewares import get_current_user +get_current_user() ``` -`MODEL_MODIFICATION_USER_FIELD` is a constant that lies in `djangoldp_notification.middlewares` and -`instance` is the instance of your model before save in DB. - # Signals ## Create notification on subscribed objects diff --git a/djangoldp_notification/check_integrity.py b/djangoldp_notification/check_integrity.py index 77839b4090e3ed6fcb73b9e9620bba3238f7f21f..b4f779eae5ada4c8ee2c45bd6f5759e52316bfbc 100644 --- a/djangoldp_notification/check_integrity.py +++ b/djangoldp_notification/check_integrity.py @@ -3,7 +3,6 @@ from django.conf import settings from django.db import models from djangoldp.models import Model from djangoldp_notification.models import send_request, Subscription -from djangoldp_notification.middlewares import MODEL_MODIFICATION_USER_FIELD class technical_user: urlid = settings.BASE_URL @@ -71,7 +70,6 @@ def check_integrity(options): continue except ObjectDoesNotExist: continue - setattr(resource, MODEL_MODIFICATION_USER_FIELD, technical_user) try: send_request(subscription.inbox, url_resource, resource, False) sent+=1 diff --git a/djangoldp_notification/filters.py b/djangoldp_notification/filters.py deleted file mode 100644 index e3201fc74d1064a80644ca57400a301575327645..0000000000000000000000000000000000000000 --- a/djangoldp_notification/filters.py +++ /dev/null @@ -1,18 +0,0 @@ -from djangoldp.filters import LDPPermissionsFilterBackend - - -class InboxFilterBackend(LDPPermissionsFilterBackend): - def filter_queryset(self, request, queryset, view): - if not request.user.is_anonymous: - return queryset.filter(user=request.user) - else: - from djangoldp_notification.models import Notification - return Notification.objects.none() - - -class SubscriptionsFilterBackend(LDPPermissionsFilterBackend): - def filter_queryset(self, request, queryset, view): - if request.method == "OPTIONS": - return queryset - else: - return super().filter_queryset(request, queryset, view) diff --git a/djangoldp_notification/middlewares.py b/djangoldp_notification/middlewares.py index 19ef25160248332047336f10399bc98601cf2bf3..1f0505fe85f87f61ec7216bc24db51ff1ef6580f 100644 --- a/djangoldp_notification/middlewares.py +++ b/djangoldp_notification/middlewares.py @@ -1,34 +1,12 @@ -from django.db.models import signals - - -MODEL_MODIFICATION_USER_FIELD = 'modification_user' - +from threading import local +_thread_locals = local() class CurrentUserMiddleware: - def __init__(self, get_response=None): + def __init__(self, get_response): self.get_response = get_response - def __call__(self, request): - self.process_request(request) - response = self.get_response(request) - signals.pre_save.disconnect(dispatch_uid=request) - signals.pre_delete.disconnect(dispatch_uid=request) - return response - - def process_request(self, request): - if request.method in ('GET', 'HEAD', 'OPTION'): - # this request shouldn't update anything - # so no signal handler should be attached - return - - if hasattr(request, 'user') and request.user.is_authenticated: - user = request.user - else: - user = None - - def _update_users(sender, instance, **kwargs): - if(type(instance).__name__ != "ScheduledActivity" and type(instance).__name__ != "LogEntry" and type(instance).__name__ != "Activity"): - setattr(instance, MODEL_MODIFICATION_USER_FIELD, user) + _thread_locals._current_user = getattr(request, 'user', None) + return self.get_response(request) - signals.pre_save.connect(_update_users, dispatch_uid=request, weak=False) - signals.pre_delete.connect(_update_users, dispatch_uid=request, weak=False) +def get_current_user(): + return getattr(_thread_locals, '_current_user', None) \ No newline at end of file diff --git a/djangoldp_notification/migrations/0016_alter_notification_options_and_more.py b/djangoldp_notification/migrations/0016_alter_notification_options_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..8371f7094950e5b209effdf4aa32c9f79fd7e5c2 --- /dev/null +++ b/djangoldp_notification/migrations/0016_alter_notification_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.3 on 2023-10-12 09:14 + +from django.db import migrations +import djangoldp.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_notification', '0015_alter_subscription_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='notification', + options={'default_permissions': {'control', 'delete', 'view', 'add', 'change'}, 'ordering': ['-date']}, + ), + migrations.AlterModelOptions( + name='subscription', + options={'default_permissions': {'control', 'delete', 'view', 'add', 'change'}, 'ordering': ['pk']}, + ), + migrations.AlterField( + model_name='notification', + name='urlid', + field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='notificationsetting', + name='urlid', + field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='subscription', + name='urlid', + field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True), + ), + ] diff --git a/djangoldp_notification/models.py b/djangoldp_notification/models.py index 5e7010541631ae4276adc694e01fa7ea1709ae08..3ac8eadb3f02d45cfc65cf5325601ca6c6128197 100644 --- a/djangoldp_notification/models.py +++ b/djangoldp_notification/models.py @@ -4,17 +4,16 @@ 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 +from django.urls import NoReverseMatch, get_resolver from django.utils.translation import gettext_lazy as _ from djangoldp.fields import LDPUrlField from djangoldp.models import Model -from djangoldp.views import LDPViewSet -from djangoldp.activities.services import ActivityQueueService, ActivityPubService, activity_sending_finished -from djangoldp_notification.middlewares import MODEL_MODIFICATION_USER_FIELD -from djangoldp_notification.permissions import InboxPermissions, SubscriptionsPermissions +from djangoldp.permissions import CreateOnly, AuthenticatedOnly, ReadAndCreate, OwnerPermissions +from djangoldp.activities.services import ActivityQueueService, activity_sending_finished +from djangoldp_notification.middlewares import get_current_user from djangoldp_notification.views import LDPNotificationsViewSet import logging @@ -34,10 +33,7 @@ class Notification(Model): class Meta(Model.Meta): owner_field = 'user' ordering = ['-date'] - permission_classes = [InboxPermissions] - anonymous_perms = ['add'] - authenticated_perms = ['inherit'] - owner_perms = ['view', 'change', 'control'] + permission_classes = [CreateOnly|OwnerPermissions] view_set = LDPNotificationsViewSet # NOTE: this would be our ideal cache behaviour @@ -81,9 +77,7 @@ class NotificationSetting(Model): class Meta: auto_author = 'user' owner_field = 'user' - anonymous_perms = [] - authenticated_perms = [] - owner_perms = ['view', 'change'] + permission_classes = [OwnerPermissions] container_path = 'settings/' serializer_fields = ['@id', 'receiveMail'] rdf_type = 'sib:usersettings' @@ -104,9 +98,7 @@ class Subscription(Model): class Meta(Model.Meta): ordering = ['pk'] - anonymous_perms = [] - authenticated_perms = ["add", "view", "delete"] - permission_classes = [SubscriptionsPermissions] + permission_classes = [AuthenticatedOnly, ReadAndCreate] @receiver(post_save, sender=Subscription, dispatch_uid="nested_subscriber_check") @@ -116,7 +108,7 @@ def create_nested_subscribers(sender, instance, created, **kwargs): try: #Â object is a WebID.. convert to local representation local = Model.resolve(instance.object.replace(settings.SITE_URL, ''))[0] - nested_fields = Model.get_meta(local, 'nested_fields', []) + nested_fields = getattr(local._meta, 'nested_fields', []) # Don't create nested subscriptions for user model (Notification loop issue) if local._meta.model_name == get_user_model()._meta.model_name: @@ -153,63 +145,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", "Migration"] 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): - 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" - +def send_request(target, object_iri, instance, request_type): + author = getattr(get_current_user(), 'urlid', 'unknown') # local inbox if target.startswith(settings.SITE_URL): user = Model.resolve_parent(target.replace(settings.SITE_URL, '')) @@ -225,6 +209,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 diff --git a/djangoldp_notification/permissions.py b/djangoldp_notification/permissions.py deleted file mode 100644 index d9fce393310027ea7fbccc62fd2d8f8b90f37911..0000000000000000000000000000000000000000 --- a/djangoldp_notification/permissions.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.contrib.auth import get_user_model - -from djangoldp.permissions import LDPPermissions -from djangoldp_notification.filters import InboxFilterBackend, SubscriptionsFilterBackend -from rest_framework.reverse import reverse - - -class InboxPermissions(LDPPermissions): - filter_backends = [InboxFilterBackend] - - -class SubscriptionsPermissions(LDPPermissions): - filter_backends = [SubscriptionsFilterBackend] - - def has_permission(self, request, view): - if request.user.is_anonymous and not request.method == "OPTIONS": - return False - - if request.method in ["GET", "PATCH", "DELETE", "PUT"]: - return True - - return super().has_permission(request, view) - - def has_object_permission(self, request, view, obj): - if request.user.is_anonymous and not request.method == "OPTIONS": - return False - - reverse_path_key = "{}-notification-list".format(get_user_model()._meta.object_name.lower()) - user_inbox = reverse(reverse_path_key, kwargs={"slug": request.user.slug}, request=request) - if obj.inbox == user_inbox: - return True - - return False diff --git a/djangoldp_notification/tests/test_cache.py b/djangoldp_notification/tests/test_cache.py index ffe1f9df9510997c990c6fa9d5006aa2c74dd377..e49ab2b4e4dafe2c071edc08c66d99570af4d02a 100644 --- a/djangoldp_notification/tests/test_cache.py +++ b/djangoldp_notification/tests/test_cache.py @@ -1,10 +1,6 @@ import uuid import json from rest_framework.test import APITestCase, APIClient - -from django.conf import settings -from djangoldp.models import Model -from djangoldp.serializers import GLOBAL_SERIALIZER_CACHE from djangoldp_account.models import LDPUser from djangoldp_notification.models import Notification @@ -74,13 +70,13 @@ class TestSubscription(APITestCase): my_container_urlid = '{}/users/{}/inbox/'.format(settings.SITE_URL, self.user.username) their_container_urlid = '{}/users/{}/inbox/'.format(settings.SITE_URL, other_user.username) - self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(Model.get_meta(Notification, 'label'), my_container_urlid)) - self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(Model.get_meta(Notification, 'label'), their_container_urlid)) + self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(getattr(Notification._meta, 'label', None), my_container_urlid)) + self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(getattr(Notification._meta, 'label', None), their_container_urlid)) # save my notification - should wipe the cache for my inbox... notification.unread = False notification.save() - self.assertFalse(GLOBAL_SERIALIZER_CACHE.has(Model.get_meta(Notification, 'label'), my_container_urlid)) + self.assertFalse(GLOBAL_SERIALIZER_CACHE.has(getattr(Notification._meta, 'label', None), my_container_urlid)) # ...but not for theirs - self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(Model.get_meta(Notification, 'label'), their_container_urlid))''' + self.assertTrue(GLOBAL_SERIALIZER_CACHE.has(getattr(Notification._meta, 'label', None), their_container_urlid))''' diff --git a/setup.cfg b/setup.cfg index 854a0b520fa1ca55bdbcd0a41cfb2e065cb63327..4f5d16405127a9722e31a166abc24a0fbb85684a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,8 +10,8 @@ license = MIT [options] packages = find: install_requires = - djangoldp~=3.0 - djangoldp_account>=3.0 + djangoldp~=3.1.0 + djangoldp_account~=3.1.0 [options.extras_require] include_package_data = True