Skip to content

Commit

Permalink
Merge pull request #8 from Amsterdam/feature/122879-case-events
Browse files Browse the repository at this point in the history
Add case events
  • Loading branch information
NvdLaan authored Aug 7, 2024
2 parents 6989e30 + 74b7361 commit f8bd1d5
Show file tree
Hide file tree
Showing 19 changed files with 363 additions and 30 deletions.
11 changes: 10 additions & 1 deletion app/apps/cases/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
from django.db import models

from apps.events.models import CaseEvent, ModelEventEmitter

class Case(models.Model):

class Case(ModelEventEmitter):
description = models.TextField(null=True)
EVENT_TYPE = CaseEvent.TYPE_CASE

def __str__(self):
return f"Case: {self.id}"

def __get_event_values__(self):
return {"description": self.description}

def __get_case__(self):
return self


class CaseStateType(models.Model):
name = models.CharField(max_length=255, unique=True)
Expand Down
31 changes: 3 additions & 28 deletions app/apps/cases/tests/tests_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.contrib.auth import get_user_model
from django.core import management
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.test import APITestCase

from utils.test_utils import get_authenticated_client, get_unauthenticated_client


class CaseApiTest(APITestCase):
Expand Down Expand Up @@ -44,28 +44,3 @@ def _create_case(self):

response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)


def get_unauthenticated_client():
"""
Returns an unauthenticated APIClient, for unit testing API requests
"""
return APIClient()


def get_authenticated_client():
"""
Returns an authenticated APIClient, for unit testing API requests
"""
user = get_test_user()
access_token = RefreshToken.for_user(user).access_token
client = APIClient()
client.credentials(HTTP_AUTHORIZATION="Bearer {}".format(access_token))
return client


def get_test_user():
"""
Creates and returns a test user
"""
return get_user_model().objects.get_or_create(email="admin@admin.com")[0]
2 changes: 2 additions & 0 deletions app/apps/cases/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from apps.events.mixins import CaseEventsMixin
from apps.workflow.models import CaseWorkflow
from apps.workflow.serializers import CaseWorkflowSerializer
from rest_framework import mixins, viewsets
Expand All @@ -9,6 +10,7 @@


class CaseViewSet(
CaseEventsMixin,
viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
Expand Down
Empty file added app/apps/events/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions app/apps/events/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib import admin

from apps.events.models import CaseEvent


admin.site.register(
CaseEvent,
admin.ModelAdmin,
readonly_fields=("date_created", "event_values"),
list_display=(
"id",
"emitter",
"emitter_id",
"emitter_type",
"type",
"date_created",
),
list_filter=(
"date_created",
"type",
),
search_fields=("emitter_id",),
)
6 changes: 6 additions & 0 deletions app/apps/events/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class EventsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.events"
62 changes: 62 additions & 0 deletions app/apps/events/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 5.0.6 on 2024-08-07 09:24

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("cases", "0002_casestatetype"),
("contenttypes", "0002_remove_content_type_name"),
]

operations = [
migrations.CreateModel(
name="CaseEvent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date_created", models.DateTimeField(auto_now_add=True)),
(
"type",
models.CharField(
choices=[
("CASE", "CASE"),
("CASE_CLOSE", "CASE_CLOSE"),
("GENERIC_TASK", "GENERIC_TASK"),
],
max_length=250,
),
),
("emitter_id", models.PositiveIntegerField()),
(
"case",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="events",
to="cases.case",
),
),
(
"emitter_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
"ordering": ["date_created"],
},
),
]
Empty file.
32 changes: 32 additions & 0 deletions app/apps/events/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from apps.cases.models import Case
from apps.events.serializers import CaseEventSerializer
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response

logger = logging.getLogger(__name__)


class CaseEventsMixin:
@action(detail=True, methods=["get"], serializer_class=CaseEventSerializer)
def events(self, request, pk):
try:
case = Case.objects.get(pk=pk)
except Case.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)

try:
events = case.events.all()
serialized_events = CaseEventSerializer(data=events, many=True)
serialized_events.is_valid()

return Response(serialized_events.data)

except Exception as e:
logger.error(f"Could not retrieve events for pk {pk}: {e}")
return Response(
{"error": "Could not retrieve events"},
status=status.HTTP_400_BAD_REQUEST,
)
108 changes: 108 additions & 0 deletions app/apps/events/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models


class CaseEvent(models.Model):

TYPE_CASE = "CASE"
TYPE_CASE_CLOSE = "CASE_CLOSE"
TYPE_GENERIC_TASK = "GENERIC_TASK"
TYPES = (
(TYPE_CASE, TYPE_CASE),
(TYPE_CASE_CLOSE, TYPE_CASE_CLOSE),
(TYPE_GENERIC_TASK, TYPE_GENERIC_TASK),
)

date_created = models.DateTimeField(auto_now_add=True)
case = models.ForeignKey(
to="cases.Case",
on_delete=models.CASCADE,
related_name="events",
)
type = models.CharField(max_length=250, null=False, blank=False, choices=TYPES)
emitter_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
emitter_id = models.PositiveIntegerField()
emitter = GenericForeignKey("emitter_type", "emitter_id")

@property
def event_values(self):
"""
Returns a dictionary with event values retrieved from Emitter object
"""
event_values = self.emitter.__get_event_values__()
event_values.pop("variables", None)
return event_values

@property
def event_variables(self):
from collections import OrderedDict

"""
Returns a dictionary with event values retrieved from Emitter object
"""
event_values = self.emitter.__get_event_values__()
variables = event_values.get("variables", {}) or {}
variables_list = OrderedDict(
sorted(
[(k, v) for k, v in variables.items()], key=lambda d: d[0], reverse=True
)
)
return variables_list

def __str__(self):
return f"{self.case.id} Case - Event {self.id} - {self.date_created}"

class Meta:
ordering = ["date_created"]


class ModelEventEmitter(models.Model):
EVENT_TYPE = None

class Meta:
abstract = True

case = None
event = GenericRelation(
CaseEvent, content_type_field="emitter_type", object_id_field="emitter_id"
)

def __get_case__(self):
if self.case:
return self.case

raise NotImplementedError("No case relation set")

def __get_event_type__(self):
if self.EVENT_TYPE:
return self.EVENT_TYPE

raise NotImplementedError("No EVENT_TYPE set")

def __get_event_values__(self):
raise NotImplementedError("Class get_values function not implemented")

def __emit_event__(self):
assert (
self.id
), "Emitter instance should exist and have an pk assigned before emitting an Event"

case = self.__get_case__()
event_type = self.__get_event_type__()

try:
CaseEvent.objects.get(emitter_id=self.id, type=event_type)
except CaseEvent.DoesNotExist:
CaseEvent.objects.create(emitter=self, type=event_type, case=case)

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.__emit_event__()


class TaskModelEventEmitter(ModelEventEmitter):
case_user_task_id = models.CharField(max_length=255, default="-1")

class Meta:
abstract = True
19 changes: 19 additions & 0 deletions app/apps/events/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from apps.events.models import CaseEvent
from rest_framework import serializers


class CaseEventSerializer(serializers.ModelSerializer):
event_values = serializers.JSONField()
event_variables = serializers.JSONField()

class Meta:
model = CaseEvent
fields = (
"id",
"event_values",
"event_variables",
"date_created",
"type",
"emitter_id",
"case",
)
Empty file.
1 change: 1 addition & 0 deletions app/apps/events/tests/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your tests here.
30 changes: 30 additions & 0 deletions app/apps/events/tests/tests_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase

from utils.test_utils import (
create_case,
get_authenticated_client,
get_unauthenticated_client,
)


class CaseEventGetAPITest(APITestCase):
def test_unauthenticated_get(self):
url = reverse("cases-events", kwargs={"pk": 1})
client = get_unauthenticated_client()
response = client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_authenticated_get_no_case(self):
url = reverse("cases-detail", kwargs={"pk": 1})
client = get_authenticated_client()
response = client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_authenticated_get_events(self):
case = create_case()
url = reverse("cases-detail", kwargs={"pk": case.id})
client = get_authenticated_client()
response = client.get(url)
self.assertEqual(case.id, response.data.get("id"))
21 changes: 21 additions & 0 deletions app/apps/events/tests/tests_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Tests for CaseEvent & EventsEmitter models
"""

from django.test import TestCase
from utils.test_utils import create_case, create_completed_task
from apps.events.models import CaseEvent


class CaseEventTest(TestCase):
def test_case_creates_events(self):
self.assertEqual(0, CaseEvent.objects.count())
create_case()
case_event_task = CaseEvent.objects.get(type=CaseEvent.TYPE_CASE)
self.assertTrue(case_event_task)

def test_completed_task_creates_events(self):
self.assertEqual(0, CaseEvent.objects.count())
create_completed_task()
case_event_task = CaseEvent.objects.get(type=CaseEvent.TYPE_GENERIC_TASK)
self.assertTrue(case_event_task)
1 change: 1 addition & 0 deletions app/apps/events/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your views here.
Loading

0 comments on commit f8bd1d5

Please sign in to comment.