Merge branch '22-send-notifications-to-parents' into 'master'

Feature: parent subscriptions

Closes #22

See merge request djangoldp-packages/djangoldp-notification!28
This commit is contained in:
Jean-Baptiste Pasquier 2020-06-08 13:02:19 +00:00
commit 64f778a52c
4 changed files with 94 additions and 21 deletions

View File

@ -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 | | `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 | | `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 # Middlewares

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -7,7 +7,7 @@ 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
from django.dispatch import receiver from django.dispatch import receiver
from django.template import loader 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 django.utils.translation import ugettext_lazy as _
from djangoldp.fields import LDPUrlField from djangoldp.fields import LDPUrlField
from djangoldp.models import Model from djangoldp.models import Model
@ -53,6 +53,8 @@ class Notification(Model):
class Subscription(Model): class Subscription(Model):
object = models.URLField() object = models.URLField()
inbox = 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): def __str__(self):
return '{}'.format(self.object) return '{}'.format(self.object)
@ -62,30 +64,42 @@ class Subscription(Model):
authenticated_perms = ["add", "view", "delete"] authenticated_perms = ["add", "view", "delete"]
permission_classes = [SubscriptionsPermissions] 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: @receiver(post_save, sender=Subscription, dispatch_uid="nested_subscriber_check")
nested_url = str(self.object) + '1/' + nested_field + '/' 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 for nested_field in nested_fields:
nested_container = Model.resolve(nested_url)[0] try:
field = local._meta.get_field(nested_field)
nested_container = field.related_model
nested_container_url = Model.absolute_url(nested_container) nested_container_url = Model.absolute_url(nested_container)
# check a Subscription on this pair doesn't exist already if field.one_to_many:
existing_subscriptions = Subscription.objects.filter(object=nested_container_url, inbox=self.inbox) # get the nested view set
# save a Subscription on this container nested_url = str(instance.object) + '1/' + nested_field + '/'
if not existing_subscriptions.exists(): view, args, kwargs = get_resolver().resolve(nested_url.replace(settings.SITE_URL, ''))
Subscription.objects.create(object=nested_container_url, inbox=self.inbox, is_backlink=True) # get the reverse name for the field
except: field_name = view.initkwargs['nested_related_name']
pass
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 --- # --- SUBSCRIPTION SYSTEM ---
@ -94,6 +108,7 @@ class Subscription(Model):
def send_notification(sender, instance, **kwargs): def send_notification(sender, instance, **kwargs):
if sender != Notification: if sender != Notification:
threads = [] threads = []
recipients = []
try: try:
url_container = settings.BASE_URL + Model.container_id(instance) url_container = settings.BASE_URL + Model.container_id(instance)
url_resource = settings.BASE_URL + Model.resource_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 # 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(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, process = Thread(target=send_request, args=[subscription.inbox, url_resource, instance,
kwargs.get("created", False)]) kwargs.get("created", False)])
process.start() process.start()
threads.append(process) threads.append(process)
recipients.append(subscription.inbox)
def send_request(target, object_iri, instance, created): def send_request(target, object_iri, instance, created):