From 4e6cf37303f01f4b323fe3571859fbab5120d106 Mon Sep 17 00:00:00 2001 From: claasga Date: Fri, 15 Nov 2024 20:18:24 +0100 Subject: [PATCH] wip added observer on log, transformer for log entries, MonPolyRunner. But Monpoly isn't available as a command yet --- .gitignore | 5 + .../deployment/django/Dockerfile | 24 +++ .../deployment/monpoly/Dockerfile | 28 ++++ .../game/channel_notifications.py | 157 +++++++++++++----- .../game/consumers/trainer_consumer.py | 17 ++ .../0007_logentry_category_logentry_type.py | 51 ++++++ .../game/migrations/0009_logrulefile.py | 22 +++ ...10_alter_logrulefile_signature_and_more.py | 32 ++++ .../migrations/0011_delete_logrulefile.py | 16 ++ .../dps_training_k/game/models/exercise.py | 7 + .../dps_training_k/game/models/log_entry.py | 84 +++++++++- .../game/serializers/log_entry_serializer.py | 5 +- .../dps_training_k/game/templmon/__init__.py | 2 + backend/dps_training_k/game/templmon/kdps.sig | 17 ++ .../dps_training_k/game/templmon/log_rule.py | 126 ++++++++++++++ .../game/templmon/log_rules/name_0.mfotl | 1 + .../game/templmon/log_transformer.py | 54 ++++++ .../game/templmon/personnel_check.mfotl | 6 + .../game/templmon/subprocess_test copy.py | 73 ++++++++ 19 files changed, 678 insertions(+), 49 deletions(-) create mode 100644 backend/dps_training_k/deployment/monpoly/Dockerfile create mode 100644 backend/dps_training_k/game/migrations/0007_logentry_category_logentry_type.py create mode 100644 backend/dps_training_k/game/migrations/0009_logrulefile.py create mode 100644 backend/dps_training_k/game/migrations/0010_alter_logrulefile_signature_and_more.py create mode 100644 backend/dps_training_k/game/migrations/0011_delete_logrulefile.py create mode 100644 backend/dps_training_k/game/templmon/__init__.py create mode 100644 backend/dps_training_k/game/templmon/kdps.sig create mode 100644 backend/dps_training_k/game/templmon/log_rule.py create mode 100644 backend/dps_training_k/game/templmon/log_rules/name_0.mfotl create mode 100644 backend/dps_training_k/game/templmon/log_transformer.py create mode 100644 backend/dps_training_k/game/templmon/personnel_check.mfotl create mode 100644 backend/dps_training_k/game/templmon/subprocess_test copy.py diff --git a/.gitignore b/.gitignore index 190bbef9..2dc1e08c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ backend/.vscode/settings.json .idea/ .vscode/launch.json backend/dps_training_k/restart_backend.sh +backend/dps_training_k/game/migrations/0008_remove_logentry_message_logentry_content_and_more.py +_build/.filesystem-clock +_build/.lock +_build/log +.gitignore diff --git a/backend/dps_training_k/deployment/django/Dockerfile b/backend/dps_training_k/deployment/django/Dockerfile index 3b140f16..c74f35fa 100644 --- a/backend/dps_training_k/deployment/django/Dockerfile +++ b/backend/dps_training_k/deployment/django/Dockerfile @@ -21,10 +21,34 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libpq-dev gcc python3-dev musl-dev \ # Translations dependencies gettext \ + # OCaml and MonPoly dependencies + git opam \ # cleaning up unused files && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* +# Initialize OPAM and switch to the required OCaml version +RUN opam init -y --disable-sandboxing && \ + opam switch create 4.11.1 && \ + eval $(opam env) && \ + opam install dune -y + +# Clone the MonPoly repository +RUN git clone https://bitbucket.org/monpoly/monpoly.git /home/opam/monpoly + +# Set the working directory +WORKDIR /home/opam/monpoly + +# Install dependencies +RUN eval $(opam env) && opam install --deps-only -y . + +# Build and install MonPoly +RUN eval $(opam env) && dune build --release && dune install +ENV PATH="/home/opam/.opam/4.11.1/bin:${PATH}" +# Verify MonPoly installation and PATH +RUN eval $(opam env) &&which monpoly && echo $PATH + +WORKDIR ${APP_HOME} # Install requirements RUN pip install -r requirements.txt diff --git a/backend/dps_training_k/deployment/monpoly/Dockerfile b/backend/dps_training_k/deployment/monpoly/Dockerfile new file mode 100644 index 00000000..3e5377a4 --- /dev/null +++ b/backend/dps_training_k/deployment/monpoly/Dockerfile @@ -0,0 +1,28 @@ +# Use an official OCaml base image +FROM ocaml/opam:debian-10-ocaml-4.11 + +# Install necessary packages +RUN sudo apt-get update && sudo apt-get install -y \ + git \ + opam + +# Initialize OPAM and switch to the required OCaml version +RUN opam init -y --disable-sandboxing && \ + opam switch create 4.11.1 && \ + eval $(opam env) + +# Clone the MonPoly repository +RUN git clone https://bitbucket.org/monpoly/monpoly.git + +# Set the working directory +WORKDIR /home/opam/monpoly + +# Install dependencies +RUN opam install --deps-only -y . + +# Build and install MonPoly +RUN dune build --release && \ + dune install + +# Set the entrypoint to monpoly +ENTRYPOINT ["monpoly"] \ No newline at end of file diff --git a/backend/dps_training_k/game/channel_notifications.py b/backend/dps_training_k/game/channel_notifications.py index 066c57f7..03447612 100644 --- a/backend/dps_training_k/game/channel_notifications.py +++ b/backend/dps_training_k/game/channel_notifications.py @@ -1,4 +1,5 @@ import logging +import asyncio from asgiref.sync import async_to_sync from channels.layers import get_channel_layer @@ -184,34 +185,39 @@ def create_trainer_log(cls, applied_action, changes, is_updated): return message = None + category = models.LogEntry.CATEGORIES.ACTION + type = None + content = {} + content["name"] = applied_action.name send_personnel_and_material = False if applied_action.state_name == models.ActionInstanceStateNames.IN_PROGRESS: - message = f'"{applied_action.name}" wurde gestartet' + type = models.LogEntry.TYPES.STARTED send_personnel_and_material = True elif applied_action.state_name == models.ActionInstanceStateNames.FINISHED: - message = f'"{applied_action.name}" wurde abgeschlossen' + type = models.LogEntry.TYPES.FINISHED if applied_action.template.category == template.Action.Category.PRODUCTION: named_produced_resources = { material.name: amount for material, amount in applied_action.template.produced_resources().items() } - message += f" und hat {str(named_produced_resources)} produziert" + content["produced"] = str(named_produced_resources) elif ( applied_action.state_name == models.ActionInstanceStateNames.CANCELED and applied_action.states.filter( name=models.ActionInstanceStateNames.IN_PROGRESS ).exists() ): - message = f'"{applied_action.name}" wurde abgebrochen' + type = models.LogEntry.TYPES.CANCELED elif applied_action.state_name == models.ActionInstanceStateNames.IN_EFFECT: - message = f'"{applied_action.name}" beginnt zu wirken' + type = models.LogEntry.TYPES.IN_EFFECT elif applied_action.state_name == models.ActionInstanceStateNames.EXPIRED: - message = f'"{applied_action.name}" wirkt nicht mehr' - + type = models.LogEntry.TYPES.EXPIRED if message and send_personnel_and_material: log_entry = models.LogEntry.objects.create( exercise=applied_action.exercise, - message=message, + category=category, + type=type, + content=content, patient_instance=applied_action.patient_instance, area=applied_action.destination_area, is_dirty=True, @@ -229,7 +235,9 @@ def create_trainer_log(cls, applied_action, changes, is_updated): elif message: log_entry = models.LogEntry.objects.create( exercise=applied_action.exercise, - message=message, + category=category, + type=type, + content=content, patient_instance=applied_action.patient_instance, area=applied_action.destination_area, ) @@ -276,15 +284,16 @@ def dispatch_event(cls, obj, changes, is_updated): def create_trainer_log(cls, exercise, changes, is_updated): if not is_updated: return - message = "" if ( changes and "state" in changes and exercise.state == models.Exercise.StateTypes.RUNNING ): - message = "Übung gestartet" - if message: - models.LogEntry.objects.create(exercise=exercise, message=message) + models.LogEntry.objects.create( + exercise=exercise, + category=models.LogEntry.CATEGORIES.EXERCISE, + type=models.LogEntry.TYPES.STARTED, + ) @classmethod def get_exercise(cls, exercise): @@ -306,7 +315,39 @@ def _notify_exercise_end_event(cls, exercise): cls._notify_group(channel, event) -class LogEntryDispatcher(ChannelNotifier): +class Observable: + _exercise_subscribers = {} + + @classmethod + def subscribe_to_exercise(cls, exercise, subscriber, send_past_logs=False): + if exercise in cls._exercise_subscribers: + cls._exercise_subscribers[exercise].add(subscriber) + else: + cls._exercise_subscribers[exercise] = {subscriber} + if send_past_logs: + past_logs = models.LogEntry.objects.filter(exercise=exercise) + for log_entry in past_logs: + subscriber.receive_log_entry(log_entry) + + @classmethod + def unsubscribe_from_exercise(cls, exercise, subscriber): + if exercise in cls._exercise_subscribers: + cls._exercise_subscribers[exercise].remove(subscriber) + + @classmethod + def _puplish_obj(cls, obj, exercise): + if exercise in cls._exercise_subscribers: + for subscriber in cls._exercise_subscribers[exercise]: + asyncio.run(subscriber.receive_log_entry(obj)) + + +class LogEntryDispatcher(Observable, ChannelNotifier): + + @classmethod + def save_and_notify(cls, obj, changes, save_origin, *args, **kwargs): + super().save_and_notify(obj, changes, save_origin, *args, **kwargs) + cls._puplish_obj(obj, obj.exercise) + @classmethod def get_group_name(cls, exercise): return f"{exercise.__class__.__name__}_{exercise.id}_log" @@ -349,15 +390,20 @@ def create_trainer_log(cls, material, changes, is_updated): changes_set = set(changes) if changes else set() assignment_changes = {"patient_instance", "area", "lab"} + category = models.LogEntry.CATEGORIES.MATERIAL + type = None + content = {"name": material.name} if not is_updated: - message = f"{material.name} ist erschienen" + type = models.LogEntry.TYPES.ARRIVED if material.area: - message += f" in {material.area}" + content["location"] = str(material.area) if material.lab: - message += f" in {material.lab}" + content["location"] = str(material.lab) log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(material), - message=message, + category=category, + type=type, + content=content, is_dirty=True, ) log_entry.materials.add(material) @@ -365,15 +411,18 @@ def create_trainer_log(cls, material, changes, is_updated): return if changes_set & assignment_changes: - message = f"{material.name} wurde zugewiesen" + type = models.LogEntry.TYPES.ASSIGNED current_location = material.attached_instance() + content["location_type"] = current_location.frontend_model_name() + content["location_name"] = current_location.name log_entry = None if isinstance(current_location, models.PatientInstance): - message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(material), - message=message, + category=category, + type=type, + content=content, patient_instance=current_location, is_dirty=True, ) @@ -381,15 +430,18 @@ def create_trainer_log(cls, material, changes, is_updated): message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(material), - message=message, + category=category, + type=type, + content=content, area=current_location, is_dirty=True, ) if isinstance(current_location, models.Lab): - message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(material), - message=message, + category=category, + type=type, + content=content, is_dirty=True, ) if log_entry: @@ -426,38 +478,44 @@ def dispatch_event(cls, patient_instance, changes, is_updated): @classmethod def create_trainer_log(cls, patient_instance, changes, is_updated): changes_set = set(changes) if changes else set() + category = models.LogEntry.CATEGORIES.PATIENT + type = None message = None + content = {"name": patient_instance.name, "code": str(patient_instance.code)} if not is_updated: - message = f"Patient*in {patient_instance.name}({patient_instance.code}) wurde eingeliefert." + type = models.LogEntry.TYPES.ARRIVED if ( patient_instance.static_information and patient_instance.static_information.injury ): - message += f" Patient*in hat folgende Verletzungen: {patient_instance.static_information.injury}" + content["injuries"] = str(patient_instance.static_information.injury) elif changes and "triage" in changes: # get_triage_display gets the long version of a ChoiceField - message = f"Patient*in {patient_instance.name} wurde triagiert auf {patient_instance.get_triage_display()}" + content["level"] = str(patient_instance.get_triage_display()) + type = models.LogEntry.TYPES.TRIAGED elif changes and changes_set & cls.location_changes: - message = f"Patient*in {patient_instance.name} wurde verlegt" + type = models.LogEntry.TYPES.MOVED current_location = patient_instance.attached_instance() + content["location_type"] = current_location.frontend_model_name() + content["location_name"] = str(current_location.name) - if isinstance(current_location, models.Area): - message += f" nach {current_location.frontend_model_name()} {current_location.name}" - if isinstance(current_location, models.Lab): - message += f" nach {current_location.frontend_model_name()} {current_location.name}" if message: if patient_instance.area: models.LogEntry.objects.create( exercise=cls.get_exercise(patient_instance), - message=message, + content=content, + category=category, + type=type, patient_instance=patient_instance, area=patient_instance.area, ) else: models.LogEntry.objects.create( exercise=cls.get_exercise(patient_instance), - message=message, + category=category, + type=type, + content=content, patient_instance=patient_instance, ) @@ -507,16 +565,19 @@ def dispatch_event(cls, personnel, changes, is_updated): @classmethod def create_trainer_log(cls, personnel, changes, is_updated): changes_set = set(changes) if changes else set() - + content = {"name": personnel.name} if not is_updated: - message = f"{personnel.name} ist eingetroffen" if personnel.area: - message += f" in {personnel.area}" + content["location_type"] = personnel.area.frontend_model_name() + content["location_name"] = personnel.area.name if personnel.lab: - message += f" in {personnel.lab}" + content["location_type"] = personnel.lab.frontend_model_name() + content["location_name"] = personnel.lab.name log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(personnel), - message=message, + category=models.LogEntry.CATEGORIES.PERSONNEL, + type=models.LogEntry.TYPES.ARRIVED, + content=content, is_dirty=True, ) log_entry.personnel.add(personnel) @@ -524,31 +585,35 @@ def create_trainer_log(cls, personnel, changes, is_updated): return if changes_set & cls.assignment_changes: - message = f"{personnel.name} wurde zugewiesen" current_location = personnel.attached_instance() + content["location_type"] = current_location.frontend_model_name() + content["location_name"] = current_location.name log_entry = None if isinstance(current_location, models.PatientInstance): - message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(personnel), - message=message, + category=models.LogEntry.CATEGORIES.PERSONNEL, + type=models.LogEntry.TYPES.ASSIGNED, + content=content, patient_instance=current_location, is_dirty=True, ) if isinstance(current_location, models.Area): - message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(personnel), - message=message, + category=models.LogEntry.CATEGORIES.PERSONNEL, + type=models.LogEntry.TYPES.UNASSIGNED, + content=content, area=current_location, is_dirty=True, ) if isinstance(current_location, models.Lab): - message += f" zu {current_location.frontend_model_name()} {current_location.name}" log_entry = models.LogEntry.objects.create( exercise=cls.get_exercise(personnel), - message=message, + category=models.LogEntry.CATEGORIES.PERSONNEL, + type=models.LogEntry.TYPES.ASSIGNED, + content=content, is_dirty=True, ) if log_entry: diff --git a/backend/dps_training_k/game/consumers/trainer_consumer.py b/backend/dps_training_k/game/consumers/trainer_consumer.py index 4992ceaa..c088119c 100644 --- a/backend/dps_training_k/game/consumers/trainer_consumer.py +++ b/backend/dps_training_k/game/consumers/trainer_consumer.py @@ -33,6 +33,8 @@ class TrainerIncomingMessageTypes: PERSONNEL_ADD = "personnel-add" PERSONNEL_DELETE = "personnel-delete" PERSONNEL_RENAME = "personnel-rename" + LOG_RULE_ADD = "log-rule-add" + LOG_RULE_DELETE = "log-rule-delete" class TrainerOutgoingMessageTypes: LOG_UPDATE = "log-update" @@ -114,6 +116,15 @@ def __init__(self, *args, **kwargs): "personnelId", "personnelName", ), + self.TrainerIncomingMessageTypes.LOG_RULE_ADD: ( + self.handle_add_log_rule, + "logRuleName", + "fields", + ), + self.TrainerIncomingMessageTypes.LOG_RULE_DELETE: ( + self.handle_delete_log_rule, + "logRuleId", + ), } self.REQUESTS_MAP.update(trainer_request_map) @@ -352,6 +363,12 @@ def handle_rename_personnel(self, _, personnel_id, personnel_name): personnel.name = personnel_name personnel.save(update_fields=["name"]) + def handle_add_log_rule(self, _, log_rule_name, fields): + pass + + def handle_delete_log_rule(self, _, log_rule_id): + pass + # ------------------------------------------------------------------------------------------------------------------------------------------------ # methods used internally # ------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/backend/dps_training_k/game/migrations/0007_logentry_category_logentry_type.py b/backend/dps_training_k/game/migrations/0007_logentry_category_logentry_type.py new file mode 100644 index 00000000..d6aac547 --- /dev/null +++ b/backend/dps_training_k/game/migrations/0007_logentry_category_logentry_type.py @@ -0,0 +1,51 @@ +# Generated by Django 5.0.1 on 2024-11-13 15:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0006_remove_area_unique_area_names_per_exercise"), + ] + + operations = [ + migrations.AddField( + model_name="logentry", + name="category", + field=models.CharField( + blank=True, + choices=[ + ("AC", "action"), + ("EX", "exercise"), + ("MA", "material"), + ("PE", "personnel"), + ("PA", "patient"), + ], + max_length=2, + null=True, + ), + ), + migrations.AddField( + model_name="logentry", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("AR", "arrived"), + ("AS", "assigned"), + ("UA", "unassigned"), + ("ST", "started"), + ("FI", "finished"), + ("FR", "finished with result"), + ("CA", "canceled"), + ("IE", "in effect"), + ("EX", "expired"), + ("MO", "moved"), + ("TR", "triaged"), + ], + max_length=2, + null=True, + ), + ), + ] diff --git a/backend/dps_training_k/game/migrations/0009_logrulefile.py b/backend/dps_training_k/game/migrations/0009_logrulefile.py new file mode 100644 index 00000000..e8ae8f68 --- /dev/null +++ b/backend/dps_training_k/game/migrations/0009_logrulefile.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.1 on 2024-11-14 12:32 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game', '0008_remove_logentry_message_logentry_content_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='LogRuleFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('signature', models.CharField(default='/app/game/templmon/kdps.sig', max_length=255)), + ('templatefile', models.FileField(storage=django.core.files.storage.FileSystemStorage(location='/app/game/templmon/log_rules/'), upload_to='log_rules/')), + ], + ), + ] diff --git a/backend/dps_training_k/game/migrations/0010_alter_logrulefile_signature_and_more.py b/backend/dps_training_k/game/migrations/0010_alter_logrulefile_signature_and_more.py new file mode 100644 index 00000000..72827125 --- /dev/null +++ b/backend/dps_training_k/game/migrations/0010_alter_logrulefile_signature_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.1 on 2024-11-14 12:46 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0009_logrulefile"), + ] + + operations = [ + migrations.AlterField( + model_name="logrulefile", + name="signature", + field=models.CharField( + default="/home/claas/Desktop/BP/dps.training_k/backend/dps_training_k/game/templmon/kdps.sig", + max_length=255, + ), + ), + migrations.AlterField( + model_name="logrulefile", + name="templatefile", + field=models.FileField( + storage=django.core.files.storage.FileSystemStorage( + location="/home/claas/Desktop/BP/dps.training_k/backend/dps_training_k/game/templmon/log_rules/" + ), + upload_to="log_rules/", + ), + ), + ] diff --git a/backend/dps_training_k/game/migrations/0011_delete_logrulefile.py b/backend/dps_training_k/game/migrations/0011_delete_logrulefile.py new file mode 100644 index 00000000..1fb62698 --- /dev/null +++ b/backend/dps_training_k/game/migrations/0011_delete_logrulefile.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.1 on 2024-11-14 15:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('game', '0010_alter_logrulefile_signature_and_more'), + ] + + operations = [ + migrations.DeleteModel( + name='LogRuleFile', + ), + ] diff --git a/backend/dps_training_k/game/models/exercise.py b/backend/dps_training_k/game/models/exercise.py index a59739cc..452d8cab 100644 --- a/backend/dps_training_k/game/models/exercise.py +++ b/backend/dps_training_k/game/models/exercise.py @@ -6,6 +6,7 @@ from .lab import Lab from .scheduled_event import ScheduledEvent from .log_entry import LogEntry +from ..templmon.log_rule import LogRuleRunner class Exercise(NonEventable, models.Model): @@ -56,6 +57,12 @@ def start_exercise(self): self.update_state(Exercise.StateTypes.RUNNING) for patient in owned_patients: patient.apply_pretreatments() + # ToDo: Add logrulerunner for testing purposes + from ..channel_notifications import LogEntryDispatcher + + test_log_runner = LogRuleRunner(self, LogEntryDispatcher) + print("LogRuleRunner created") + test_log_runner.start_log_rule() def save(self, *args, **kwargs): changes = kwargs.get("update_fields", None) diff --git a/backend/dps_training_k/game/models/log_entry.py b/backend/dps_training_k/game/models/log_entry.py index 0b76fe6e..30dd98b6 100644 --- a/backend/dps_training_k/game/models/log_entry.py +++ b/backend/dps_training_k/game/models/log_entry.py @@ -5,6 +5,25 @@ class LogEntry(models.Model): + class CATEGORIES(models.TextChoices): + ACTION = "AC", "action" + EXERCISE = "EX", "exercise" + MATERIAL = "MA", "material" + PERSONNEL = "PE", "personnel" + PATIENT = "PA", "patient" + + class TYPES(models.TextChoices): + ARRIVED = "AR", "arrived" + ASSIGNED = "AS", "assigned" + UNASSIGNED = "UA", "unassigned" + STARTED = "ST", "started" + FINISHED = "FI", "finished" + CANCELED = "CA", "canceled" + IN_EFFECT = "IE", "in effect" + EXPIRED = "EX", "expired" + MOVED = "MO", "moved" + TRIAGED = "TR", "triaged" + class Meta: constraints = [ models.UniqueConstraint( @@ -17,7 +36,13 @@ class Meta: timestamp = models.DateTimeField( null=True, blank=True, help_text="May only be set while exercise is running" ) - message = models.TextField() + category = models.CharField( + choices=CATEGORIES.choices, max_length=2, blank=True, null=True + ) # need to remove blank and null when production + type = models.CharField( + choices=TYPES.choices, max_length=2, blank=True, null=True + ) # need to remove blank and null when production + content = models.JSONField(blank=True, null=True, default=None) is_dirty = models.BooleanField( default=False, help_text="Set to True if objects is missing relevant Keys (e.g. timestamp)", @@ -30,6 +55,10 @@ class Meta: materials = models.ManyToManyField("MaterialInstance", blank=True) personnel = models.ManyToManyField("Personnel", blank=True) + @property + def message(self): + return self._verbosify_content(self.content) + def save(self, *args, **kwargs): if self._state.adding: if self.exercise.is_running(): @@ -54,7 +83,9 @@ def set_empty_timestamps(cls, exercise): return log_entries def generate_local_id(self, exercise): - highest_local_id = LogEntry.objects.filter(exercise=exercise).aggregate(models.Max("local_id"))["local_id__max"] + highest_local_id = LogEntry.objects.filter(exercise=exercise).aggregate( + models.Max("local_id") + )["local_id__max"] if highest_local_id: return highest_local_id + 1 return 1 @@ -67,3 +98,52 @@ def is_valid(self): def set_dirty(self, new_dirty): self.is_dirty = new_dirty self.save(update_fields=["is_dirty"]) + + def _verbosify_content(self, content): + if not content: + return "" + message = "" + type_to_submessage = { + self.TYPES.ARRIVED: "ist erschienen", + self.TYPES.ASSIGNED: "wurde zugewiesen", + self.TYPES.UNASSIGNED: "wurde freigegeben", + self.TYPES.STARTED: "wurde gestartet", + self.TYPES.FINISHED: "wurde abgeschlossen", + self.TYPES.CANCELED: "wurde abgebrochen", + self.TYPES.IN_EFFECT: "beginnt zu wirken", + self.TYPES.EXPIRED: "wirkt nicht mehr", + self.TYPES.MOVED: "wurde verlegt", + self.TYPES.TRIAGED: "wurde triagiert", + } + if self.category == self.CATEGORIES.ACTION: + message += f"{(content['name'])} {type_to_submessage[self.type]}" + message += ( + ("und hat " + content["produced"] + " produziert") + if "produced" in content + else "" + ) + elif self.category == self.CATEGORIES.EXERCISE: + message += f"Übung {type_to_submessage[self.type]}" + elif self.category == self.CATEGORIES.MATERIAL: + message += f"{content['name']} {type_to_submessage[self.type]}" + if self.type == self.TYPES.ASSIGNED: + message += f" zu {content['location_type']} {content['location_name']}" + elif self.category == self.CATEGORIES.PATIENT: + type_to_submessage[self.TYPES.ARRIVED] = "wurde eingeliefert" + message += f"Patient*in {content['name']}({content['code']}) {type_to_submessage[self.type]}" + if "injuries" in content: + message += f" mit den folgenden Verletzungen: {content['injuries']}" + elif "level" in content: + message += f" mit der Triage-Stufe {content['level']}" + elif "location_category" in content: + message += ( + f" nach {content['location_type']} {content['location_name']}" + ) + elif self.category == self.CATEGORIES.PERSONNEL: + type_to_submessage[self.TYPES.ARRIVED] = "ist eingetroffen" + message += f"{content['name']} {type_to_submessage[self.type]}" + if self.type == self.TYPES.ARRIVED: + message += f" in {content['location_type']} {content['location_name']}" + elif self.type == self.TYPES.ASSIGNED: + message += f" zu {content['location_type']} {content['location_name']}" + return message diff --git a/backend/dps_training_k/game/serializers/log_entry_serializer.py b/backend/dps_training_k/game/serializers/log_entry_serializer.py index cdb0369e..5f691714 100644 --- a/backend/dps_training_k/game/serializers/log_entry_serializer.py +++ b/backend/dps_training_k/game/serializers/log_entry_serializer.py @@ -7,7 +7,7 @@ class LogEntrySerializer(serializers.ModelSerializer): logId = serializers.IntegerField(source="local_id") - logMessage = serializers.CharField(source="message") + logMessage = serializers.SerializerMethodField() logTime = serializers.SerializerMethodField() areaId = serializers.SerializerMethodField() patientId = serializers.SerializerMethodField() @@ -37,6 +37,9 @@ def get_logTime(self, obj): timestamp = obj.timestamp.replace(tzinfo=pytz.UTC) return int(timestamp.timestamp() * 1000) + def get_logMessage(self, obj): + return obj.message + def get_areaId(self, obj): return obj.area.pk if obj.area else None diff --git a/backend/dps_training_k/game/templmon/__init__.py b/backend/dps_training_k/game/templmon/__init__.py new file mode 100644 index 00000000..79bbd5e3 --- /dev/null +++ b/backend/dps_training_k/game/templmon/__init__.py @@ -0,0 +1,2 @@ +from .log_rule import LogRule +from .log_transformer import * diff --git a/backend/dps_training_k/game/templmon/kdps.sig b/backend/dps_training_k/game/templmon/kdps.sig new file mode 100644 index 00000000..178822c2 --- /dev/null +++ b/backend/dps_training_k/game/templmon/kdps.sig @@ -0,0 +1,17 @@ +assign_personnel(string, string) +unassign_personnel(string) +personnel_arrived(string, string) + +patient_action_started(string, string) +patient_action_canceled(string, string) +patient_action_finished(string, string) +patient_examination_result(string, string, string) + +assign_device(string, string) +unassign_device(string) + +patient_arrived(string, string, string, string) +patient_relocated(string, string, string) +changed_state(string, float, float, string) +triage(string, string) +unknown_log_type(string) diff --git a/backend/dps_training_k/game/templmon/log_rule.py b/backend/dps_training_k/game/templmon/log_rule.py new file mode 100644 index 00000000..01afcfb1 --- /dev/null +++ b/backend/dps_training_k/game/templmon/log_rule.py @@ -0,0 +1,126 @@ +import os +import asyncio +import threading +from .log_transformer import transform + + +class LogRule: + # There may be only one LogRule instance with the same values for each exercise + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + SUB_DIR = "log_rules" # ToDo + RULE_FILENAME = "mfotl" + DEFAULT_SIGNATURE = os.path.join(BASE_DIR, "kdps.sig") + _count = -1 + + def __init__(self, signature, templatefile, log_transformer): + self.signature = signature + self.templatefile = templatefile + self.log_transformer = log_transformer + + # def __del__(self): + # os.remove(self.templatefile) + + @classmethod + def create(cls, rule: str, name: str, log_transformer): + file_path = os.path.join( + cls.BASE_DIR, cls.SUB_DIR, cls._generate_temp_file_name(name) + ) + with open(file_path, "w") as f: + f.write(rule) + return cls(cls.DEFAULT_SIGNATURE, file_path, log_transformer) + + @classmethod + def _get_unique_number(cls): + cls._count += 1 + return cls._count + + @classmethod + def _generate_temp_file_name(cls, name): + return f"{name}_{cls._get_unique_number()}.{cls.RULE_FILENAME}" + + +class LogRuleRunner: + # I need to start monpoly before subscribing to the log or store the log entries + loop = asyncio.new_event_loop() + + def __init__(self, exercise, log): + print("init called with ", str(log)) + self.log = log + self.exercise = exercise + + def __del__(self): + pass + + async def receive_log_entry(self, log_entry): + print("*******************************************") + print("Received log entry: ", log_entry.pk) + monpolified_log_entry = transform(log_entry) + self.monpoly.stdin.write(monpolified_log_entry.encode()) + await self.monpoly.stdin.drain() + + async def read_output(self, process): + self.monpoly_started_event.set() + while True: + line = await process.stdout.readline() + if not line: + break + print(f"Received: {line.decode('utf-8')[:-1]}") + + async def _launch_monpoly( + self, loop: asyncio.AbstractEventLoop, mfotl_path, sig_path, rewrite=True + ): + """Has to be launched in a separate thread""" + monpoly = await asyncio.create_subprocess_exec( + "monpoly", + "-sig", + sig_path, + "-formula", + mfotl_path, + "" if rewrite else "-no_rw", + env=os.environ.copy(), # Ensure the environment variables are passed + ) + loop.call_soon_threadsafe( + target=lambda: asyncio.create_task(self.read_output(monpoly)) + ) + await self.monpoly_started_event.wait() + return monpoly + + def start_log_rule(self): + print( + "Current state of the loop: Is it running?" + str(self.loop.is_running()) + if self.loop + else "No loop" + ) + + def launch_listener_loop(loop: asyncio.AbstractEventLoop): + asyncio.set_event_loop(loop) + loop.run_forever() + + self.monpoly_started_event = threading.Event() + if (not self.loop) or (not self.loop.is_running()): + threading.Thread(target=launch_listener_loop, args=(self.loop,)).start() + print("started loop") + print("Trying to run monpoly thread") + self.monpoly = asyncio.run( + self._launch_monpoly( + self.loop, + "/home/claas/Desktop/BP/dps.training_k/backend/dps_training_k/game/templmon/personnel_check.mfotl", + "/home/claas/Desktop/BP/dps.training_k/backend/dps_training_k/game/templmon/kdps.sig", + ) + ) + print("Succeeded starting monpoly") + self.log.subscribe_to_exercise(self.exercise, self, send_past_logs=True) + print("Subscribed to exercise") + + # startup extra asyncio_thread as loop + # asyncio.run(lauch_monpoly()) + # event.wait() + # log.subscribe_to_exercise(exercise, self, send_past_logs=True) + # + # launch_monpoly() + # monpoly = await asyncio.create_subprocess_exec() + # listener_thread = loop.call_soon_threadsafe(asyncio.create_task, listener) oder eventuell + # + # + # + # atexitstuff diff --git a/backend/dps_training_k/game/templmon/log_rules/name_0.mfotl b/backend/dps_training_k/game/templmon/log_rules/name_0.mfotl new file mode 100644 index 00000000..841e840f --- /dev/null +++ b/backend/dps_training_k/game/templmon/log_rules/name_0.mfotl @@ -0,0 +1 @@ +rule \ No newline at end of file diff --git a/backend/dps_training_k/game/templmon/log_transformer.py b/backend/dps_training_k/game/templmon/log_transformer.py new file mode 100644 index 00000000..9d041a52 --- /dev/null +++ b/backend/dps_training_k/game/templmon/log_transformer.py @@ -0,0 +1,54 @@ +import game.models.log_entry as le + + +class MonpolyLogEntry: + PERSONNEL_ARRIVED = "personnel_arrived" + ASSIGNED_PERSONNEL = "assigned_personnel" + UNASSIGNED_PERSONNEL = "unassigned_personnel" + PATIENT_ARRIVED = "patient_arrived" + UNKNOW_LOG_TYPE = "unknown_log_type" + + +def determine_log_type(log_entry: le.LogEntry): + if log_entry.category == le.LogEntry.CATEGORIES.PERSONNEL: + if log_entry.type == le.LogEntry.TYPES.ARRIVED: + return MonpolyLogEntry.PERSONNEL_ARRIVED + elif log_entry.type == le.LogEntry.TYPES.ASSIGNED: + return MonpolyLogEntry.ASSIGNED_PERSONNEL + elif log_entry.type == le.LogEntry.TYPES.UNASSIGNED: + return MonpolyLogEntry.UNASSIGNED_PERSONNEL + elif log_entry.category == le.LogEntry.CATEGORIES.PATIENT: + if log_entry.type == le.LogEntry.TYPES.ARRIVED: + return MonpolyLogEntry.PATIENT_ARRIVED + else: + return MonpolyLogEntry.UNKNOW_LOG_TYPE + + +def generate_timestamp(log_entry: le.LogEntry): + return str(log_entry.timestamp.timestamp()) + + +def transform(log_entry: le.LogEntry): + log_type = determine_log_type(log_entry) + timestamp = generate_timestamp(log_entry) + log_str = f"@{timestamp} " + + if log_type == MonpolyLogEntry.ASSIGNED_PERSONNEL: + personnel_id = log_entry.personnel[0].pk + patient_id = log_entry.patient_instance.pk + log_str += f"assigned_personnel({personnel_id}, {patient_id})" + elif log_type == MonpolyLogEntry.UNASSIGNED_PERSONNEL: + personnel_id = log_entry.personnel[0].pk + log_str += f"unassigned_personnel({personnel_id})" + elif log_type == MonpolyLogEntry.PATIENT_ARRIVED: + patient_id = log_entry.patient_instance.pk + area_id = log_entry.area.pk + triage_display = log_entry.patient_instance.get_triage_display + injuries = log_entry.content["injuries"] + log_str += ( + f"patient_arrived({patient_id}, {area_id}, {triage_display}, {injuries})" + ) + else: + log_str += f"unknown_log_type({log_entry.pk})" + + return log_str diff --git a/backend/dps_training_k/game/templmon/personnel_check.mfotl b/backend/dps_training_k/game/templmon/personnel_check.mfotl new file mode 100644 index 00000000..b7402e46 --- /dev/null +++ b/backend/dps_training_k/game/templmon/personnel_check.mfotl @@ -0,0 +1,6 @@ + (personnel_count <- CNT personnel_id;patient_id + (NOT unassign_personnel(personnel_id)) + SINCE[0,*] + assign_personnel(personnel_id, patient_id)) +AND + (personnel_count >= 4) \ No newline at end of file diff --git a/backend/dps_training_k/game/templmon/subprocess_test copy.py b/backend/dps_training_k/game/templmon/subprocess_test copy.py new file mode 100644 index 00000000..9981851f --- /dev/null +++ b/backend/dps_training_k/game/templmon/subprocess_test copy.py @@ -0,0 +1,73 @@ +import subprocess +import asyncio +import os + +tasks = [] + +log = """@10 assign_personnel(per_n_1, pat_n_1) +@13 assign_personnel(per_n_2, pat_n_1) +@16 assign_personnel(per_n_3, pat_n_1) +@19 assign_personnel(per_n_4, pat_n_1) +@19.5 unknown_log_type(lalalalal) +@20 assign_personnel(per_n_5, pat_n_1) +@21 unassign_personnel(per_n_1) +@22 unassign_personnel(per_n_2) +@30 assign_personnel(per_n_1, pat_n_1) +@31 assign_personnel(per_n_2, pat_n_1) +@32 unassign_personnel(per_n_3) +@33 assign_personnel(per_n_3, pat_n_2) +@35 assign_personnel(per_n_6, pat_n_2) +@37 assign_personnel(per_n_7, pat_n_2) +@39 assign_personnel(per_n_8, pat_n_2)""" + +# Get the absolute path of the current directory +base_dir = os.path.dirname(os.path.abspath(__file__)) + + +async def read_output(process): + while True: + line = await process.stdout.readline() + if not line: + break + print(f"Received: {line.decode('utf-8')[:-1]}") + + +async def launch_task(*args): + monpoly = await asyncio.create_subprocess_exec( + *args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + tasks.append(monpoly) + asyncio.create_task(read_output(monpoly)) + + +async def send_input(subprocess, line): + subprocess.stdin.write(line.encode()) + await subprocess.stdin.drain() + + +async def main(): + for i in range(500): + await asyncio.create_task( + launch_task( + "monpoly", + "-sig", + os.path.join(base_dir, "kdps.sig"), + "-formula", + os.path.join(base_dir, "personnel_check.mfotl"), + ) + ) + + for line in log.split("\n"): + for task in tasks: + await send_input(task, line + "\n") + await asyncio.sleep(1) # might be turned off + + print(f"Sent: {line}") + await asyncio.sleep(1) + + +asyncio.run(main())