From 7141746875b64eda735431d513047ac2b1f94093 Mon Sep 17 00:00:00 2001 From: Tom Clark Date: Wed, 7 Feb 2024 10:45:45 +0000 Subject: [PATCH 1/6] FIX: Reinstate fieldsets for admin and make test server boot --- django_twined/admin/admin.py | 35 +++++++++++++++----------------- django_twined/admin/fieldsets.py | 25 +++++++++++++++++++++++ pyproject.toml | 5 +---- tests/server/asgi.py | 7 ++----- 4 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 django_twined/admin/fieldsets.py diff --git a/django_twined/admin/admin.py b/django_twined/admin/admin.py index a4153be..6b17180 100644 --- a/django_twined/admin/admin.py +++ b/django_twined/admin/admin.py @@ -8,6 +8,14 @@ from jsoneditor.forms import JSONEditor from octue.log_handlers import LOG_RECORD_ATTRIBUTES_WITH_TIMESTAMP, create_octue_formatter +from .fieldsets import ( + question_basic_fieldset, + question_delivery_ack_fieldset, + question_exceptions_fieldset, + question_log_records_fieldset, + question_monitor_messages_fieldset, + question_result_fieldset, +) from .mixins import CreatableFieldsMixin @@ -48,25 +56,14 @@ class QuestionAdmin(admin.ModelAdmin): ) fieldsets = ( - ( - None, - { - "fields": ( - "id", - "status", - "service_revision", - "asked", - "answered", - "latest_heartbeat", - ) - }, - ), - ("Inputs", {"classes": ("collapse",), "fields": ("input_values",)}), - ("Delivery Acknowledgement", {"classes": ("collapse",), "fields": ("delivery_acknowledgement",)}), - ("Log Records", {"classes": ("collapse",), "fields": ("log_records",)}), - ("Monitor Messages", {"classes": ("collapse",), "fields": ("monitor_messages",)}), - ("Result", {"classes": ("collapse",), "fields": ("result",)}), - ("Exceptions", {"classes": ("collapse",), "fields": ("exceptions",)}), + question_basic_fieldset, + # question_db_input_values_fieldset, + # question_db_output_values_fieldset, + question_delivery_ack_fieldset, + question_log_records_fieldset, + question_monitor_messages_fieldset, + question_result_fieldset, + question_exceptions_fieldset, ) # @staticmethod diff --git a/django_twined/admin/fieldsets.py b/django_twined/admin/fieldsets.py new file mode 100644 index 0000000..11232da --- /dev/null +++ b/django_twined/admin/fieldsets.py @@ -0,0 +1,25 @@ +question_basic_fieldset = ( + None, + { + "fields": ( + "id", + "status", + "service_revision", + "asked", + "answered", + "latest_heartbeat", + ) + }, +) + +question_db_input_values_fieldset = ("Input Values", {"classes": ("collapse",), "fields": ("input_values",)}) +question_db_output_values_fieldset = ("Output Values", {"classes": ("collapse",), "fields": ("output_values",)}) + +question_delivery_ack_fieldset = ( + "Delivery Acknowledgement", + {"classes": ("collapse",), "fields": ("delivery_acknowledgement",)}, +) +question_log_records_fieldset = ("Log Records", {"classes": ("collapse",), "fields": ("log_records",)}) +question_monitor_messages_fieldset = ("Monitor Messages", {"classes": ("collapse",), "fields": ("monitor_messages",)}) +question_result_fieldset = ("Result", {"classes": ("collapse",), "fields": ("result",)}) +question_exceptions_fieldset = ("Exceptions", {"classes": ("collapse",), "fields": ("exceptions",)}) diff --git a/pyproject.toml b/pyproject.toml index fdfa5c4..90a11a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-twined" -version = "0.7.1" +version = "0.7.2" description = "A django app to manage octue services" authors = ["Tom Clark ", "Marcus Lugg "] license = "MIT" @@ -10,11 +10,8 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", ] repository = "https://github.com/octue/django-twined" diff --git a/tests/server/asgi.py b/tests/server/asgi.py index a6fefe5..0e20b8b 100644 --- a/tests/server/asgi.py +++ b/tests/server/asgi.py @@ -1,5 +1,4 @@ -import django_twined.routing -from channels.routing import ProtocolTypeRouter, URLRouter +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application @@ -7,6 +6,4 @@ # The main django application which you're writing an app for will need to set up something similar -application = ProtocolTypeRouter( - {"http": get_asgi_application(), "websocket": URLRouter(django_twined.routing.websocket_urlpatterns)} -) +application = ProtocolTypeRouter({"http": get_asgi_application()}) From 84333ac07600e830348c6b36b458bbed0e409f97 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 7 Feb 2024 13:56:54 +0000 Subject: [PATCH 2/6] FIX: Fix elif statement --- django_twined/signals/receivers.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django_twined/signals/receivers.py b/django_twined/signals/receivers.py index 1059ac1..e4f93e9 100644 --- a/django_twined/signals/receivers.py +++ b/django_twined/signals/receivers.py @@ -55,7 +55,7 @@ def receive_event(sender, event_kind, event_reference, event_payload, event_para if event_kind == "delivery_acknowledgement": delivery_acknowledgement_received.send(sender=ServiceUsageEvent, service_usage_event=sue) - if event_kind == "exception": + elif event_kind == "exception": exception_received.send(sender=ServiceUsageEvent, service_usage_event=sue) elif event_kind == "heartbeat": diff --git a/pyproject.toml b/pyproject.toml index fdfa5c4..288b18e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-twined" -version = "0.7.1" +version = "0.7.2" description = "A django app to manage octue services" authors = ["Tom Clark ", "Marcus Lugg "] license = "MIT" From 6bfa17d6110e601d3f1fadb291fd0d079ce4f6e6 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 7 Feb 2024 14:22:09 +0000 Subject: [PATCH 3/6] ENH: Add `duration` field to question admin --- django_twined/admin/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/django_twined/admin/admin.py b/django_twined/admin/admin.py index a4153be..a34af5e 100644 --- a/django_twined/admin/admin.py +++ b/django_twined/admin/admin.py @@ -134,6 +134,14 @@ def result(obj): """ return obj.result.data + @staticmethod + def duration(obj): + """Show the time it took to answer the question in seconds. + + :return int: + """ + return (obj.answered - obj.asked).seconds + def ask_question(self, obj): """Override this to ask a question using an async task queue or other method. This will ask the question directly.""" obj.ask() From 4a0075efe51b9fc42ffb4bd6016f9d207b9a1484 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 7 Feb 2024 14:25:13 +0000 Subject: [PATCH 4/6] ENH: Add duration to question basic fieldset --- django_twined/admin/fieldsets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_twined/admin/fieldsets.py b/django_twined/admin/fieldsets.py index 11232da..a0be70e 100644 --- a/django_twined/admin/fieldsets.py +++ b/django_twined/admin/fieldsets.py @@ -8,6 +8,7 @@ "asked", "answered", "latest_heartbeat", + "duration", ) }, ) From 2ab715fde998f5fb4eb9547316b7fabfa0ca3958 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 7 Feb 2024 14:25:25 +0000 Subject: [PATCH 5/6] FIX: Return duration as `None` when ask/answer time is not populated --- django_twined/admin/admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django_twined/admin/admin.py b/django_twined/admin/admin.py index ea12138..9e1c86b 100644 --- a/django_twined/admin/admin.py +++ b/django_twined/admin/admin.py @@ -135,9 +135,10 @@ def result(obj): def duration(obj): """Show the time it took to answer the question in seconds. - :return int: + :return int|None: """ - return (obj.answered - obj.asked).seconds + if obj.answered and obj.asked: + return (obj.answered - obj.asked).seconds def ask_question(self, obj): """Override this to ask a question using an async task queue or other method. This will ask the question directly.""" From 47da2bf7a35319e5d75d5c2231cf7874ea862909 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 7 Feb 2024 14:33:45 +0000 Subject: [PATCH 6/6] REF: Make `duration` a property of `AbstractQuestion` --- django_twined/admin/admin.py | 9 --------- django_twined/models/questions.py | 9 +++++++++ tests/test_models/test_questions.py | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/django_twined/admin/admin.py b/django_twined/admin/admin.py index 9e1c86b..6b17180 100644 --- a/django_twined/admin/admin.py +++ b/django_twined/admin/admin.py @@ -131,15 +131,6 @@ def result(obj): """ return obj.result.data - @staticmethod - def duration(obj): - """Show the time it took to answer the question in seconds. - - :return int|None: - """ - if obj.answered and obj.asked: - return (obj.answered - obj.asked).seconds - def ask_question(self, obj): """Override this to ask a question using an async task queue or other method. This will ask the question directly.""" obj.ask() diff --git a/django_twined/models/questions.py b/django_twined/models/questions.py index ecd8132..7b68fc8 100644 --- a/django_twined/models/questions.py +++ b/django_twined/models/questions.py @@ -60,6 +60,15 @@ def status_message(self): """ return STATUS_MESSAGE_MAP[self.status] + @property + def duration(self): + """Show the time it took to answer the question in seconds. + + :return int|None: + """ + if self.answered and self.asked: + return (self.answered - self.asked).seconds + def get_duplicate(self, save=True): """Duplicate the question instance and optionally save to the database""" kwargs = {} diff --git a/tests/test_models/test_questions.py b/tests/test_models/test_questions.py index 411fe14..6e3c657 100644 --- a/tests/test_models/test_questions.py +++ b/tests/test_models/test_questions.py @@ -2,7 +2,9 @@ # pylint: disable=missing-docstring # pylint: disable=protected-access # pylint: disable=too-many-public-methods +import datetime import os +import time from unittest import skipIf from unittest.mock import patch @@ -18,6 +20,12 @@ class QuestionTestCase(TestCase): + def test_unasked_question_duration(self): + """Test that the duration of an unasked question is `None`.""" + sr = ServiceRevision.objects.create(name="test-service") + q = QuestionWithValuesDatabaseStorage.objects.create(service_revision=sr) + self.assertIsNone(q.duration) + @patch("django_twined.models.ServiceRevision.ask", return_value=("subscription", "question_uuid")) def test_ask_question(self, mock): """Ensures that a question can be asked""" @@ -32,6 +40,21 @@ def test_ask_question(self, mock): self.assertIn("input_manifest", mock.call_args.kwargs) self.assertIn("question_attribute", mock.call_args.kwargs["input_values"]) + # Check that the duration is `None` as the question hasn't been answered. + self.assertIsNone(q.duration) + + @patch("django_twined.models.ServiceRevision.ask", return_value=("subscription", "question_uuid")) + def test_answered_question_duration(self, mock): + """Test that the duration of an answered question is a non-zero integer.""" + sr = ServiceRevision.objects.create(name="test-service") + q = QuestionWithValuesDatabaseStorage.objects.create(service_revision=sr) + q.input_values = {"question_attribute": "1"} + q.ask() + + time.sleep(1) + q.answered = datetime.datetime.now(tz=datetime.timezone.utc) + self.assertTrue(q.duration > 0) + def test_input_values_get_saved(self): """Ensures that input values get saved on the mixin. I'm testing this because the objects.create() doesn't accept the input_values