From 598baa5fa1215d337f1cdc41d2b349a152897229 Mon Sep 17 00:00:00 2001 From: Calum Mackervoy Date: Mon, 8 Jun 2020 13:02:19 +0000 Subject: [PATCH] Feature: parent subscriptions --- README.md | 3 + .../migrations/0006_subscription_parent.py | 21 ++++++ .../migrations/0007_auto_20200604_1055.py | 24 +++++++ djangoldp_notification/models.py | 67 +++++++++++++------ 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 djangoldp_notification/migrations/0006_subscription_parent.py create mode 100644 djangoldp_notification/migrations/0007_auto_20200604_1055.py diff --git a/README.md b/README.md index 4d249b2..cf1ca48 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ 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 | + +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 # Middlewares diff --git a/djangoldp_notification/migrations/0006_subscription_parent.py b/djangoldp_notification/migrations/0006_subscription_parent.py new file mode 100644 index 0000000..71d2447 --- /dev/null +++ b/djangoldp_notification/migrations/0006_subscription_parent.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-06-02 10:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_notification', '0005_auto_20200505_1733'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='djangoldp_notification.Subscription'), + ), + ] diff --git a/djangoldp_notification/migrations/0007_auto_20200604_1055.py b/djangoldp_notification/migrations/0007_auto_20200604_1055.py new file mode 100644 index 0000000..b16cee2 --- /dev/null +++ b/djangoldp_notification/migrations/0007_auto_20200604_1055.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-06-04 10:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangoldp_notification', '0006_subscription_parent'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscription', + name='parent', + ), + migrations.AddField( + model_name='subscription', + name='field', + field=models.CharField(blank=True, help_text='if set to a field name on the object model, the field will be passed instead of the object instance', max_length=255, null=True), + ), + ] diff --git a/djangoldp_notification/models.py b/djangoldp_notification/models.py index 6915b82..e776a44 100644 --- a/djangoldp_notification/models.py +++ b/djangoldp_notification/models.py @@ -7,7 +7,7 @@ from django.db import models from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.template import loader -from django.urls import NoReverseMatch +from django.urls import NoReverseMatch, get_resolver from django.utils.translation import ugettext_lazy as _ from djangoldp.fields import LDPUrlField from djangoldp.models import Model @@ -53,6 +53,8 @@ class Notification(Model): class Subscription(Model): object = models.URLField() inbox = models.URLField() + 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') def __str__(self): return '{}'.format(self.object) @@ -62,30 +64,42 @@ class Subscription(Model): authenticated_perms = ["add", "view", "delete"] permission_classes = [SubscriptionsPermissions] - def save(self, *args, **kwargs): - # save subscriptions for nested fields - if self.pk is None and not self.is_backlink and self.object.startswith(settings.SITE_URL): - try: - # object is a WebID.. convert to local representation - local = Model.resolve(self.object.replace(settings.SITE_URL, ''))[0] - nested_fields = Model.get_meta(local, 'nested_fields', []) - for nested_field in nested_fields: - nested_url = str(self.object) + '1/' + nested_field + '/' +@receiver(post_save, sender=Subscription, dispatch_uid="nested_subscriber_check") +def create_nested_subscribers(sender, instance, created, **kwargs): + # save subscriptions for one-to-many nested fields + if created and not instance.is_backlink and instance.object.startswith(settings.SITE_URL): + 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', []) - # we have the nested_url, but we want the model contained within's container - nested_container = Model.resolve(nested_url)[0] + for nested_field in nested_fields: + try: + field = local._meta.get_field(nested_field) + nested_container = field.related_model nested_container_url = Model.absolute_url(nested_container) - # check a Subscription on this pair doesn't exist already - existing_subscriptions = Subscription.objects.filter(object=nested_container_url, inbox=self.inbox) - # save a Subscription on this container - if not existing_subscriptions.exists(): - Subscription.objects.create(object=nested_container_url, inbox=self.inbox, is_backlink=True) - except: - pass + if field.one_to_many: + # get the nested view set + nested_url = str(instance.object) + '1/' + nested_field + '/' + view, args, kwargs = get_resolver().resolve(nested_url.replace(settings.SITE_URL, '')) + # get the reverse name for the field + field_name = view.initkwargs['nested_related_name'] + + if field_name is not None and field_name != '': + # check that this nested-field subscription doesn't already exist + existing_subscriptions = Subscription.objects.filter(object=nested_container_url, inbox=instance.inbox, + field=field_name) + # save a Subscription on this container + if not existing_subscriptions.exists(): + Subscription.objects.create(object=nested_container_url, inbox=instance.inbox, is_backlink=True, + field=field_name) + except: + pass + except: + pass - super(Subscription, self).save(*args, **kwargs) # --- SUBSCRIPTION SYSTEM --- @@ -94,6 +108,7 @@ class Subscription(Model): def send_notification(sender, instance, **kwargs): if sender != Notification: threads = [] + recipients = [] try: url_container = settings.BASE_URL + Model.container_id(instance) url_resource = settings.BASE_URL + Model.resource_id(instance) @@ -102,11 +117,21 @@ def send_notification(sender, instance, **kwargs): # 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)): - if not instance.is_backlink: + if not instance.is_backlink and 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: + instance = getattr(instance, subscription.field, instance) + try: + url_resource = settings.BASE_URL + Model.resource_id(instance) + except NoReverseMatch: + continue + process = Thread(target=send_request, args=[subscription.inbox, url_resource, instance, kwargs.get("created", False)]) process.start() threads.append(process) + recipients.append(subscription.inbox) def send_request(target, object_iri, instance, created):