Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • djangoldp-packages/djangoldp-notification
  • decentral1se/djangoldp-notification
  • femmefaytale/djangoldp-notification
  • 3wc/djangoldp-notification
4 results
Show changes
Commits on Source (148)
Showing
with 594 additions and 33 deletions
......@@ -3,4 +3,7 @@ db.sqlite3
*.pyc
*.egg-info
dist
script
\ No newline at end of file
script
.idea/*
*/.idea/*
*.DS_STORE
---
image: python:3.6
image: python:3.11
include:
project: infra/gitlab
ref: master
file: templates/python.ci.yml
stages:
- test
- release
test:
stage: test
script:
- echo 'Make your tests here !'
- pip install .[dev]
- python -m unittest djangoldp_notification.tests.runner
except:
- master
- tags
tags:
- sib
- test
publish:
stage: release
before_script:
- git config user.name "${GITLAB_USER_NAME}"
- git config user.email "${GITLAB_USER_EMAIL}"
- git remote set-url origin "https://gitlab-ci-token:${GL_TOKEN}@git.happy-dev.fr/${CI_PROJECT_PATH}.git"
- pip install git+https://github.com/plup/python-semantic-release
- pip install sib-commit-parser
script:
- semantic-release publish
only:
- master
tags:
- sib
cache: []
extends: .publish_pypi
include djangoldp_notification/templates/*.html
\ No newline at end of file
graft djangoldp_notification
global‐exclude *.py[cod] __pycache__ *.so
\ No newline at end of file
......@@ -2,6 +2,131 @@
This module is an add-on for Django REST Framework, based on Django LDP add-on. It serves django models for a notifications component, respecting the Linked Data Platform convention.
It aims at enabling people with little development skills to serve their own data, to be used with a LDP application.
# Models
## Notification
A object representing a notification
\ No newline at end of file
An object representing a notification. A notification has the following fields:
| Field | Type | Default | Description |
| --------- | ---------------------- | ------- | --------------------------------------------------------- |
| `user` | `ForeignKey` to `User` | | User targeted by the notification. |
| `author` | `LDPUrlField` | | ID of the user at the origin of the notification |
| `object` | `LDPUrlField` | | ID of the object which is the subject of the notification |
| `type` | `CharField` | | Short description of the notification |
| `summary` | `TextField` | | Longer description of the notification |
| `date` | `DateTimeField` | `now` | Date of the notification |
| `unread` | `BooleanField` | `True` | Indicates that the notification has not been read yet. |
NB: You can access to all the notifications of a User at `[host]/users/[id]/inbox`
## Subscription
An object allowing a User to be notified of any change on a resource or a container. A subscription has the following fields:
| Field | Type | Default | Description |
| -------- | ---------- | ------- | ------------------------------------------------------------ |
| `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
You can automatically create required subscriptions based on your settings.py with this management command:
```bash
./manage.py create_subscriptions
```
# Middlewares
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
from djangoldp_notification.middlewares import get_current_user
get_current_user()
```
# Signals
## Create notification on subscribed objects
When an object is saved, a notification is created for all the subscriptions related to this object.
## Send email when new notification
When a notification is created, an email is sent to the user.
# Django commands
This package also bring a few Django managment command. You can use them
at the root of your SIB project.
## `mock_notification`
This lets you create mocked notifications. Useful for develpment.
Usage:
```
python3 manage.py mock_notification [--size <number_of_notifications>]
```
Will create the number of dummy notifications specified by `--size`.
By default, 0.
## `suppress_old_notifications`
Will suppress old notification. This is a maintenance command to prevent
the server to blow under the weight of your notifications.
Usage:
```
python3 manage.py suppress_old_notifications [--older <time_period>]
```
This will erase notification older than the time period specified by
`--older`. By default, 72h ago. `time_period` is expressed in minutes.
`d`, `h` and `m` suffix are also valid to express periods in days,
hours and minutes.
Examples:
```shell
# Default. Will delete notifications older than 72h
python3 manage.py mock_notification
# Default. Will delete notifications older than 10 minutes ago
python3 manage.py mock_notification --older 10
# Default. Will delete notifications older than 10 days ago
python3 manage.py mock_notification --older 10d
# Default. Will delete notifications older than 10 hours ago
python3 manage.py mock_notification --older 10h
```
## Check your datas integrity
Because of the way the DjangoLDP's federation work, you may want to send your notification to every subscribers sometimes.
Follow the [check_integrity](https://git.startinblox.com/djangoldp-packages/djangoldp#check-your-datas-integrity) command of DjangoLDP to get how much resources will be impacted:
```bash
./manage.py check_integrity
```
Then run this command to send notifications:
```bash
./manage.py check_integrity --send-subscription-notifications
```
Notice that you may have to restart your ActivityQueue or your DjangoLDP server, depending on [how you configured the ActivityQueueService](https://git.startinblox.com/djangoldp-packages/djangoldp/blob/master/djangoldp/conf/server_template/server/wsgi.py-tpl#L21-24)
## Changelog
Now supports Django4.2 and Python3.11
from django.contrib import admin
from .models import Notification, Subscription
from djangoldp.admin import DjangoLDPAdmin
from djangoldp.models import Model
from .models import Notification, NotificationSetting, Subscription
@admin.register(Notification)
class NotificationAdmin(DjangoLDPAdmin):
list_display = ('urlid', 'user', 'author', 'type', 'object', 'unread')
exclude = ('urlid', 'is_backlink', 'allow_create_backlink')
search_fields = ['urlid', 'user__urlid', 'author', 'object', 'type', 'summary']
ordering = ['-urlid']
def get_queryset(self, request):
# Hide distant notification
queryset = super(NotificationAdmin, self).get_queryset(request)
internal_ids = [x.pk for x in queryset if not Model.is_external(x)]
return queryset.filter(pk__in=internal_ids)
@admin.register(Subscription)
class SubscriptionAdmin(DjangoLDPAdmin):
list_display = ('urlid', 'object', 'inbox', 'field')
exclude = ('urlid', 'is_backlink', 'allow_create_backlink')
search_fields = ['urlid', 'object', 'inbox', 'field']
ordering = ['urlid']
@admin.register(NotificationSetting)
class NotificationSettingAdmin(DjangoLDPAdmin):
list_display = ('urlid', 'user', 'receiveMail')
exclude = ('urlid', 'is_backlink', 'allow_create_backlink')
search_fields = ['urlid', 'user', 'receiveMail']
ordering = ['urlid']
admin.site.register(Notification)
admin.site.register(Subscription)
from django.apps import apps
from django.conf import settings
from django.db import models
from djangoldp.models import Model
from djangoldp_notification.models import send_request, Subscription
class technical_user:
urlid = settings.BASE_URL
def add_arguments(parser):
parser.add_argument(
"--ignore-subscriptions",
default=False,
nargs="?",
const=True,
help="Ignore subscriptions related check",
)
parser.add_argument(
"--send-subscription-notifications",
default=False,
nargs="?",
const=True,
help="Send an update notification for every local resources to every subscribers",
)
def check_integrity(options):
print('---')
print("DjangoLDP Notification")
if(not options["ignore_subscriptions"]):
models_list = apps.get_models()
resources = set()
resources_map = dict()
for model in models_list:
for obj in model.objects.all():
if hasattr(obj, "urlid"):
if(obj.urlid):
if(obj.urlid.startswith(settings.BASE_URL)):
if(type(obj).__name__ != "ScheduledActivity" and type(obj).__name__ != "LogEntry" and type(obj).__name__ != "Activity"):
resources.add(obj.urlid)
resources_map[obj.urlid] = obj
print("Found "+str(len(resources_map))+" local resources on "+str(len(models_list))+" models for "+str(Subscription.objects.count())+" subscribers")
if(options["send_subscription_notifications"]):
sent=0
for resource in sorted(resources):
resource = resources_map[resource]
recipients = []
try:
url_container = settings.BASE_URL + Model.container_id(resource)
url_resource = settings.BASE_URL + Model.resource_id(resource)
except NoReverseMatch:
continue
for subscription in Subscription.objects.filter(models.Q(object=url_resource) | models.Q(object=url_container)):
if subscription.inbox not in recipients and not subscription.is_backlink:
# I may have configured to send the subscription to a foreign key
if subscription.field is not None and len(subscription.field) > 1:
try:
resource = getattr(resource, subscription.field, resource)
# don't send notifications for foreign resources
if hasattr(resource, 'urlid') and Model.is_external(resource.urlid):
continue
url_resource = settings.BASE_URL + Model.resource_id(resource)
except NoReverseMatch:
continue
except ObjectDoesNotExist:
continue
try:
send_request(subscription.inbox, url_resource, resource, False)
sent+=1
except:
print("Failed to create subscription activity for "+url_resource)
recipients.append(subscription.inbox)
print("Sent "+str(sent)+" activities to "+str(Subscription.objects.count())+" subscribers")
print("Depending on your server configuration, you may have to restart the ActivityQueue or the server to properly process em.")
else:
print("Send an update notification for all those resources to all of their subscribers with `./manage.py check_integrity --send-subscription-notifications`")
else:
print("Ignoring djangoldp-notification checks")
\ No newline at end of file
MIDDLEWARE = ['djangoldp_notification.middlewares.CurrentUserMiddleware']
\ No newline at end of file
import factory
from django.conf import settings
from django.apps import apps
from .models import Notification
from django.db.models.signals import post_save
......@@ -12,7 +13,7 @@ class NotificationFactory(factory.django.DjangoModelFactory):
type = factory.Faker('text', max_nb_chars=50)
summary = factory.Faker('paragraph', nb_sentences=3, variable_nb_sentences=True)
author = factory.Faker('url')
user = factory.Iterator(settings.AUTH_USER_MODEL.objects.all())
user = factory.Iterator(apps.get_model(settings.AUTH_USER_MODEL).objects.all())
date = factory.Faker('past_datetime')
unread = factory.Faker('boolean')
object = factory.Faker('url')
\ No newline at end of file
File added
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <BENOIT@STARTINBLOX.COM>, 2022.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-19 21:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: models.py:206
msgid "Auteur inconnu"
msgstr "Unknown author"
#: models.py:263
msgid "Quelqu'un"
msgstr "Someone"
#: models.py:275
msgid "le chat"
msgstr "the chat"
#: models.py:278
msgid "t'a envoyé un message privé"
msgstr "sent you a private message"
#: models.py:280
msgid "t'a mentionné sur "
msgstr "mentioned you on "
#: models.py:295
msgid "Notification sur "
msgstr "Notification on "
#: templates/email.html:8
msgid "de"
msgstr "from"
#: templates/email.html:15
msgid "Ne réponds pas directement à cet e-mail, à la place rends-toi sur"
msgstr "Do not answer directly to this email, instead go to"
File added
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-19 21:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: models.py:206
msgid "Auteur inconnu"
msgstr ""
#: models.py:263
msgid "Quelqu'un"
msgstr ""
#: models.py:275
msgid "le chat"
msgstr ""
#: models.py:278
msgid "t'a envoyé un message privé"
msgstr ""
#: models.py:280
msgid "t'a mentionné sur "
msgstr ""
#: models.py:295
msgid "Notification sur "
msgstr ""
#: templates/email.html:8
msgid "de"
msgstr ""
#: templates/email.html:15
msgid "Ne réponds pas directement à cet e-mail, à la place rends-toi sur"
msgstr ""
File added
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-19 21:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: models.py:206
msgid "Auteur inconnu"
msgstr "Auteur inconnu"
#: models.py:263
msgid "Quelqu'un"
msgstr "Quelqu'un"
#: models.py:275
msgid "le chat"
msgstr "le chat"
#: models.py:278
msgid "t'a envoyé un message privé"
msgstr "t'a envoyé un message privé"
#: models.py:280
msgid "t'a mentionné sur "
msgstr "t'a mentionné sur "
#: models.py:295
#, fuzzy
#| msgid "t'a mentionné sur "
msgid "Notification sur "
msgstr "t'a mentionné sur "
#: templates/email.html:8
msgid "de"
msgstr "de"
#: templates/email.html:15
msgid "Ne réponds pas directement à cet e-mail, à la place rends-toi sur"
msgstr "Ne réponds pas directement à cet e-mail, à la place rends-toi sur"
from django.core.management.base import BaseCommand
from django.conf import settings
from djangoldp_notification.models import Subscription
class Command(BaseCommand):
help = 'Create required subscriptions'
def handle(self, *args, **options):
host = getattr(settings, 'SITE_URL')
jabber_host = getattr(settings, 'JABBER_DEFAULT_HOST')
xmpp = getattr(settings, 'PROSODY_HTTP_URL')
try:
sub = Subscription.objects.get(object="{}/circles/".format(host))
sub.delete()
except Subscription.DoesNotExist:
pass
try:
sub = Subscription.objects.get(object="{}/projects/".format(host))
sub.delete()
except Subscription.DoesNotExist:
pass
try:
sub = Subscription.objects.get(object="{}/circle-members/".format(host))
sub.delete()
except Subscription.DoesNotExist:
pass
try:
sub = Subscription.objects.get(object="{}/project-members/".format(host))
sub.delete()
except Subscription.DoesNotExist:
pass
# Allow us to manage general information about circles and projects like name, jabberIDs, description, etc.
Subscription.objects.get_or_create(object=host+"/circles/", inbox=xmpp + "/conference." + jabber_host + "/startinblox_groups_muc_admin", field=None)
Subscription.objects.get_or_create(object=host+"/projects/", inbox=xmpp + "/conference." + jabber_host + "/startinblox_groups_muc_admin", field=None)
# The management of circle and projets members now comes from management of groups
Subscription.objects.get_or_create(object=host+"/groups/", inbox=xmpp + "/conference." + jabber_host + "/startinblox_groups_muc_members_admin", field=None)
# Allow us to manage general information about users, like their names, jabberID, avatars, etc.
Subscription.objects.get_or_create(object=host+"/users/", inbox=xmpp + "/" + jabber_host + "/startinblox_groups_user_admin", field=None)
# When we change an information about the user (like avatar and so on) we want to notify prosody
self.stdout.write(self.style.SUCCESS("Successfully created subscriptions\nhost: "+host+"\nxmpp server: "+xmpp+"\njabber host: "+jabber_host))
from textwrap import dedent
from datetime import datetime, timedelta
from django.utils import timezone
from django.core.management.base import BaseCommand, CommandError
from djangoldp_notification.models import Notification
def _compute_time_limit(timeout):
"""
>>> _compute_time_limit('10')
10
>>> _compute_time_limit('10h')
600
>>> _compute_time_limit('10m')
10
>>> _compute_time_limit('10d')
14400
"""
try:
if timeout.endswith("d"):
return int(timeout[:-1]) * 24 * 60
elif timeout.endswith("h"):
return int(timeout[:-1]) * 60
elif timeout.endswith("m"):
return int(timeout[:-1])
else:
return int(timeout)
except ValueError:
raise CommandError("--older option is not correct")
class Command(BaseCommand):
help = "Suppress notifications older than 72h"
def add_arguments(self, parser):
parser.add_argument(
"--older",
action="store",
default="72h",
help=dedent(
"""
Set a different time period for notifications to be deleted. Default is 72h
This parameters takes a interger value in minutes by default. 'd', 'h' and 'm' suffix also work.
Examples: --older 10d, --older 10
"""
),
)
def handle(self, *args, **options):
older_than = _compute_time_limit(options["older"])
limit = timezone.now() - timedelta(minutes=older_than)
Notification.objects.filter(date__lte=limit).delete()
from threading import local
_thread_locals = local()
class CurrentUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
_thread_locals._current_user = getattr(request, 'user', None)
return self.get_response(request)
def get_current_user():
return getattr(_thread_locals, '_current_user', None)
\ No newline at end of file
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2019-02-28 01:14
# Generated by Django 1.11.20 on 2019-08-28 12:01
from __future__ import unicode_literals
from django.conf import settings
......@@ -21,17 +21,18 @@ class Migration(migrations.Migration):
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author_user', djangoldp.fields.LDPUrlField()),
('object', models.URLField()),
('author', djangoldp.fields.LDPUrlField()),
('object', djangoldp.fields.LDPUrlField()),
('type', models.CharField(max_length=255)),
('summary', models.TextField()),
('date', models.DateTimeField(auto_now_add=True)),
('read', models.BooleanField()),
('unread', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inbox', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['date'],
'permissions': (('view_notification', 'Read'), ('control_notification', 'Control')),
'ordering': ['-date'],
'abstract': False,
'default_permissions': ('add', 'change', 'delete', 'view', 'control'),
},
),
migrations.CreateModel(
......@@ -42,7 +43,8 @@ class Migration(migrations.Migration):
('inbox', models.URLField()),
],
options={
'permissions': (('view_notification', 'Read'), ('control_notification', 'Control')),
'abstract': False,
'default_permissions': ('add', 'change', 'delete', 'view', 'control'),
},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2019-03-01 04:18
# Generated by Django 1.11 on 2019-09-17 11:07
from __future__ import unicode_literals
from django.db import migrations
......@@ -13,9 +13,14 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterField(
migrations.AddField(
model_name='notification',
name='object',
field=djangoldp.fields.LDPUrlField(),
name='urlid',
field=djangoldp.fields.LDPUrlField(blank=True, null=True, unique=True),
),
migrations.AddField(
model_name='subscription',
name='urlid',
field=djangoldp.fields.LDPUrlField(blank=True, null=True, unique=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2020-04-29 13:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('djangoldp_notification', '0002_auto_20190917_1107'),
]
operations = [
migrations.AddField(
model_name='notification',
name='allow_create_backlink',
field=models.BooleanField(default=True, help_text='set to False to disable backlink creation after Model save'),
),
migrations.AddField(
model_name='subscription',
name='allow_create_backlink',
field=models.BooleanField(default=True, help_text='set to False to disable backlink creation after Model save'),
),
]