-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from Amsterdam/feature/122879-case-events
Add case events
- Loading branch information
Showing
19 changed files
with
363 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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",), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Create your tests here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Create your views here. |
Oops, something went wrong.