Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial events API implementation #178

Merged
merged 7 commits into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions response/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ def ready(self):
dialog_handlers,
)

from .core import signals as core_signals # noqa: F401

site_settings.RESPONSE_LOGIN_REQUIRED = getattr(
site_settings, "RESPONSE_LOGIN_REQUIRED", True
)
3 changes: 2 additions & 1 deletion response/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.contrib import admin

from response.core.models import Action, ExternalUser, Incident
from response.core.models import Action, Event, ExternalUser, Incident

admin.site.register(Action)
admin.site.register(Event)
admin.site.register(Incident)
admin.site.register(ExternalUser)
2 changes: 2 additions & 0 deletions response/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from .action import Action
from .event import Event
from .incident import Incident
from .timeline import TimelineEvent, add_incident_update_event
from .user_external import ExternalUser

__all__ = (
"Action",
"Event",
"Incident",
"TimelineEvent",
"ExternalUser",
Expand Down
16 changes: 16 additions & 0 deletions response/core/models/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import json

from django.db import models


class Event(models.Model):
ACTION_EVENT_TYPE = "action_event"
INCIDENT_EVENT_TYPE = "incident_event"

timestamp = models.DateTimeField()
event_type = models.CharField(max_length=50)
payload = models.TextField()

def save(self, *args, **kwargs):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a getter here that parses the payload and returns a native dict, but since events are write-only this isn't currently needed.

self.payload = json.dumps(self.payload)
super().save(*args, **kwargs)
9 changes: 8 additions & 1 deletion response/core/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import emoji_data_python
from rest_framework import serializers

from response.core.models import Action, ExternalUser, Incident, TimelineEvent
from response.core.models import Action, Event, ExternalUser, Incident, TimelineEvent
from response.slack.models import CommsChannel
from response.slack.reference_utils import slack_to_human_readable

Expand Down Expand Up @@ -131,3 +131,10 @@ def update(self, instance, validated_data):

instance.save()
return instance


class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ("id", "timestamp", "event_type", "payload")
read_only_fields = ("id", "timestamp", "event_type", "payload")
58 changes: 58 additions & 0 deletions response/core/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
from datetime import datetime

from django.conf import settings
from django.db.models.signals import post_save
from django.utils.module_loading import import_string

from response.core.models import Action, Event, Incident
from response.core.serializers import ActionSerializer, IncidentSerializer

logger = logging.getLogger(__name__)


class ActionEventHandler:
def handle(sender, instance: Action, **kwargs):
logger.info(f"Handling post_save for action: {instance}")

event = Event()
event.event_type = Event.ACTION_EVENT_TYPE

# Event payload should be a dict for serializing to JSON.
event.payload = ActionSerializer(instance).data
event.payload["incident_id"] = instance.incident.pk
if "details_ui" in event.payload:
del event.payload["details_ui"]

event.timestamp = datetime.now(tz=None)
event.save()


class IncidentEventHandler:
def handle(sender, instance: Incident, **kwargs):
logger.info(f"Handling post_save for incident: {instance}")

event = Event()
event.event_type = Event.INCIDENT_EVENT_TYPE

# Event payload should be a dict for serializing to JSON.
event.payload = IncidentSerializer(instance).data
# Actions generate their own events, no need to duplicate them here.
if "action_items" in event.payload:
del event.payload["action_items"]

event.timestamp = datetime.now(tz=None)
event.save()


if hasattr(settings, "ACTION_EVENT_HANDLER_CLASS"):
cls = import_string(settings.ACTION_EVENT_HANDLER_CLASS)
post_save.connect(cls.handle, sender=Action)
else:
post_save.connect(ActionEventHandler.handle, sender=Action)

if hasattr(settings, "INCIDENT_EVENT_HANDLER_CLASS"):
cls = import_string(settings.INCIDENT_EVENT_HANDLER_CLASS)
post_save.connect(cls.handle, sender=Incident)
else:
post_save.connect(IncidentEventHandler.handle, sender=Incident)
2 changes: 2 additions & 0 deletions response/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from response.core.views import (
ActionViewSet,
EventsViewSet,
ExternalUserViewSet,
IncidentActionViewSet,
IncidentsByMonthViewSet,
Expand Down Expand Up @@ -30,6 +31,7 @@
)
router.register(r"actions", ActionViewSet)
router.register(r"users", ExternalUserViewSet)
router.register(r"events", EventsViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
Expand Down
6 changes: 6 additions & 0 deletions response/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from response.core import serializers
from response.core.models.action import Action
from response.core.models.event import Event
from response.core.models.incident import Incident
from response.core.models.timeline import TimelineEvent
from response.core.models.user_external import ExternalUser
Expand Down Expand Up @@ -77,3 +78,8 @@ def get_queryset(self):
def perform_create(self, serializer):
incident_pk = self.kwargs["incident_pk"]
serializer.save(incident_id=incident_pk)


class EventsViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = serializers.EventSerializer
28 changes: 28 additions & 0 deletions response/migrations/0014_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.2.3 on 2019-11-07 17:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("response", "0013_incident_private")]

operations = [
migrations.CreateModel(
name="Event",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timestamp", models.DateTimeField()),
("event_type", models.CharField(max_length=50)),
("payload", models.TextField()),
],
)
]
3 changes: 2 additions & 1 deletion response/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .core.models import Action, ExternalUser, Incident, TimelineEvent
from .core.models import Action, Event, ExternalUser, Incident, TimelineEvent
from .slack.models import (
CommsChannel,
HeadlinePost,
Expand All @@ -9,6 +9,7 @@

__all__ = (
"Action",
"Event",
"Incident",
"TimelineEvent",
"ExternalUser",
Expand Down
6 changes: 4 additions & 2 deletions response/serializers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from .core.serializers import (
ActionSerializer,
CommsChannelSerializer,
EventSerializer,
ExternalUserSerializer,
IncidentSerializer,
TimelineEventSerializer,
)

__all__ = (
"ExternalUserSerializer",
"TimelineEventSerializer",
"ActionSerializer",
"CommsChannelSerializer",
"EventSerializer",
"ExternalUserSerializer",
"IncidentSerializer",
"TimelineEventSerializer",
)
30 changes: 30 additions & 0 deletions tests/api/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import json

from django.urls import reverse
from rest_framework.test import force_authenticate

from response.core.views import EventsViewSet
from tests.factories.event import EventFactory


def test_list_events(arf, api_user):
persisted_events = EventFactory.create_batch(10)

req = arf.get(reverse("event-list"))
force_authenticate(req, user=api_user)
response = EventsViewSet.as_view({"get": "list"})(req)

assert response.status_code == 200, "Got non-200 response from API"
content = json.loads(response.rendered_content)

assert "results" in content, "Response didn't have results key"
events = content["results"]
assert len(events) == len(
persisted_events
), "Didn't get expected number of events back"

for event in events:
assert event["timestamp"]
assert event["event_type"]
payload = json.loads(event["payload"])
assert payload["report"]
2 changes: 2 additions & 0 deletions tests/factories/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .action import ActionFactory
from .event import EventFactory
from .incident import IncidentFactory
from .timeline import TimelineEventFactory
from .user import ExternalUserFactory, UserFactory
Expand All @@ -9,4 +10,5 @@
"UserFactory",
"ActionFactory",
"ExternalUserFactory",
"EventFactory",
)
2 changes: 2 additions & 0 deletions tests/factories/action.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import factory
from django.db.models.signals import post_save
from faker import Factory

from response.core.models import Action

faker = Factory.create()


@factory.django.mute_signals(post_save)
class ActionFactory(factory.DjangoModelFactory):
class Meta:
model = Action
Expand Down
23 changes: 23 additions & 0 deletions tests/factories/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import random

import factory
from faker import Factory

from response.core.models import Event

faker = Factory.create()


class EventFactory(factory.DjangoModelFactory):
class Meta:
model = Event

timestamp = factory.LazyFunction(
lambda: faker.date_time_between(start_date="-6m", end_date="now", tzinfo=None)
)
event_type = random.choice([Event.INCIDENT_EVENT_TYPE])

# Using an Incident/Event factory here fails with a mysterious error:
# https://github.com/pytest-dev/pytest-django/issues/713 (using @pytest.mark...
# didn't resolve it). For now, a static fixture suffices.
payload = {"report": "we're out of milk", "impact": "making tea is difficult"}