Add client interface to taiga-events.

remotes/origin/enhancement/email-actions
Andrey Antukh 2014-03-13 21:06:14 +01:00
parent 68a5fe32d3
commit 4acb59e3cf
8 changed files with 164 additions and 0 deletions

View File

@ -78,6 +78,9 @@ DJMAIL_SEND_ASYNC = True
DJMAIL_MAX_RETRY_NUMBER = 3 DJMAIL_MAX_RETRY_NUMBER = 3
DJMAIL_TEMPLATE_EXTENSION = "jinja" DJMAIL_TEMPLATE_EXTENSION = "jinja"
# Events backend
EVENTS_PUSH_BACKEND = "taiga.events.backends.postgresql.EventsPushBackend"
# Message System # Message System
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage" MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
@ -158,6 +161,7 @@ INSTALLED_APPS = [
"taiga.base.notifications", "taiga.base.notifications",
"taiga.base.searches", "taiga.base.searches",
"taiga.base", "taiga.base",
"taiga.events",
"taiga.domains", "taiga.domains",
"taiga.projects", "taiga.projects",
"taiga.projects.mixins.blocked", "taiga.projects.mixins.blocked",

0
taiga/events/__init__.py Normal file
View File

View File

@ -0,0 +1,3 @@
from .base import get_events_backend
__all__ = ["get_events_backend"]

View File

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

View File

@ -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()

42
taiga/events/changes.py Normal file
View File

@ -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")

36
taiga/events/models.py Normal file
View File

@ -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")

View File

@ -3,7 +3,12 @@ from django import test
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.http import HttpResponse 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 middleware as mw
from . import changes as ch
class SessionIDMiddlewareTests(test.TestCase): class SessionIDMiddlewareTests(test.TestCase):
@ -27,3 +32,19 @@ class SessionIDMiddlewareTests(test.TestCase):
mw_instance.process_request(request) mw_instance.process_request(request)
self.assertEqual(mw.get_current_session_id(), "foobar") 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)