Add client interface to taiga-events.
parent
68a5fe32d3
commit
4acb59e3cf
|
@ -78,6 +78,9 @@ DJMAIL_SEND_ASYNC = True
|
|||
DJMAIL_MAX_RETRY_NUMBER = 3
|
||||
DJMAIL_TEMPLATE_EXTENSION = "jinja"
|
||||
|
||||
# Events backend
|
||||
EVENTS_PUSH_BACKEND = "taiga.events.backends.postgresql.EventsPushBackend"
|
||||
|
||||
# Message System
|
||||
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
|
||||
|
||||
|
@ -158,6 +161,7 @@ INSTALLED_APPS = [
|
|||
"taiga.base.notifications",
|
||||
"taiga.base.searches",
|
||||
"taiga.base",
|
||||
"taiga.events",
|
||||
"taiga.domains",
|
||||
"taiga.projects",
|
||||
"taiga.projects.mixins.blocked",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from .base import get_events_backend
|
||||
|
||||
__all__ = ["get_events_backend"]
|
|
@ -0,0 +1,45 @@
|
|||
import abc
|
||||
import importlib
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class BaseEventsPushBackend(object, metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def emit_event(self, message:str, *, channel:str="events"):
|
||||
pass
|
||||
|
||||
|
||||
def load_class(path):
|
||||
"""
|
||||
Load class from path.
|
||||
"""
|
||||
|
||||
mod_name, klass_name = path.rsplit('.', 1)
|
||||
|
||||
try:
|
||||
mod = importlib.import_module(mod_name)
|
||||
except AttributeError as e:
|
||||
raise ImproperlyConfigured('Error importing {0}: "{1}"'.format(mod_name, e))
|
||||
|
||||
try:
|
||||
klass = getattr(mod, klass_name)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('Module "{0}" does not define a "{1}" class'.format(mod_name, klass_name))
|
||||
|
||||
return klass
|
||||
|
||||
|
||||
def get_events_backend(path:str=None, options:dict=None):
|
||||
if path is None:
|
||||
path = getattr(settings, "EVENTS_PUSH_BACKEND", None)
|
||||
|
||||
if path is None:
|
||||
raise ImproperlyConfigured("Events push system not configured")
|
||||
|
||||
if options is None:
|
||||
options = getattr(settings, "EVENTS_PUSH_BACKEND_OPTIONS", {})
|
||||
|
||||
cls = load_class(path)
|
||||
return cls(**options)
|
|
@ -0,0 +1,13 @@
|
|||
from django.db import transaction
|
||||
from django.db import connection
|
||||
|
||||
from . import base
|
||||
|
||||
|
||||
class EventsPushBackend(base.BaseEventsPushBackend):
|
||||
@transaction.atomic
|
||||
def emit_event(self, message:str, *, channel:str="events"):
|
||||
sql = "NOTIFY {channel}, %s".format(channel=channel)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(sql, [message])
|
||||
cursor.close()
|
|
@ -0,0 +1,42 @@
|
|||
import json
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from . import backends
|
||||
|
||||
watched_types = (
|
||||
("userstories", "userstory"),
|
||||
("issues", "issue"),
|
||||
)
|
||||
|
||||
|
||||
def _get_type_for_model(model_instance):
|
||||
ct = ContentType.objects.get_for_model(model_instance)
|
||||
return (ct.app_label, ct.model)
|
||||
|
||||
|
||||
def emit_change_event_for_model(model_instance, sessionid:str, *,
|
||||
type:str="change", channel:str="events"):
|
||||
"""
|
||||
Emit change event for notify of model change to
|
||||
all connected frontends.
|
||||
"""
|
||||
content_type = _get_type_for_model(model_instance)
|
||||
|
||||
assert hasattr(model_instance, "project_id")
|
||||
assert content_type in watched_types
|
||||
assert type in ("create", "change", "delete")
|
||||
|
||||
project_id = model_instance.project_id
|
||||
routing_key = "project.{0}".format(project_id)
|
||||
|
||||
data = {"type": "model-changes",
|
||||
"routing_key": routing_key,
|
||||
"session_id": sessionid,
|
||||
"data": {
|
||||
"type": type,
|
||||
"matches": ".".join(content_type),
|
||||
"pk": model_instance.pk}}
|
||||
|
||||
backend = backends.get_events_backend()
|
||||
return backend.emit_event(json.dumps(data), channel="events")
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
from django.db.models import signals
|
||||
from django.dispatch import receiver
|
||||
|
||||
from . import middleware as mw
|
||||
from . import changes
|
||||
|
||||
|
||||
@receiver(signals.post_save, dispatch_uid="events_dispatcher_on_change")
|
||||
def on_save_any_model(sender, instance, created, **kwargs):
|
||||
# Ignore any object that can not have project_id
|
||||
content_type = changes._get_type_for_model(instance)
|
||||
|
||||
# Ignore any other changes
|
||||
if content_type not in changes.watched_types:
|
||||
return
|
||||
|
||||
sesionid = mw.get_current_session_id()
|
||||
|
||||
if created:
|
||||
changes.emit_change_event_for_model(instance, sesionid, type="create")
|
||||
else:
|
||||
changes.emit_change_event_for_model(instance, sesionid, type="change")
|
||||
|
||||
|
||||
@receiver(signals.post_delete, dispatch_uid="events_dispatcher_on_delete")
|
||||
def on_delete_any_model(sender, instance, **kwargs):
|
||||
# Ignore any object that can not have project_id
|
||||
content_type = changes._get_type_for_model(instance)
|
||||
|
||||
# Ignore any other changes
|
||||
if content_type not in changes.watched_types:
|
||||
return
|
||||
|
||||
sesionid = mw.get_current_session_id()
|
||||
changes.emit_change_event_for_model(instance, sesionid, type="delete")
|
|
@ -3,7 +3,12 @@ from django import test
|
|||
from django.test.client import RequestFactory
|
||||
from django.http import HttpResponse
|
||||
|
||||
from taiga.projects.tests import create_project
|
||||
from taiga.projects.issues.tests import create_issue
|
||||
from taiga.base.users.tests import create_user
|
||||
|
||||
from . import middleware as mw
|
||||
from . import changes as ch
|
||||
|
||||
|
||||
class SessionIDMiddlewareTests(test.TestCase):
|
||||
|
@ -27,3 +32,19 @@ class SessionIDMiddlewareTests(test.TestCase):
|
|||
mw_instance.process_request(request)
|
||||
|
||||
self.assertEqual(mw.get_current_session_id(), "foobar")
|
||||
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
class ChangesTest(test.TestCase):
|
||||
fixtures = ["initial_domains.json"]
|
||||
|
||||
def test_emit_change_for_model(self):
|
||||
user = create_user(1) # Project owner
|
||||
project = create_project(1, user)
|
||||
issue = create_issue(1, user, project)
|
||||
|
||||
with patch("taiga.events.backends.get_events_backend") as mock_instance:
|
||||
ch.emit_change_event_for_model(issue, "sessionid")
|
||||
self.assertTrue(mock_instance.return_value.emit_event.called)
|
||||
|
|
Loading…
Reference in New Issue