diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 904fb43b29..7884fb399d 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -9,7 +9,7 @@ from rest_framework import filters as drf_filters from rest_framework import status from rest_framework.decorators import action -from rest_framework.exceptions import APIException, ValidationError +from rest_framework.exceptions import APIException, PermissionDenied, ValidationError from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, @@ -271,10 +271,28 @@ def operate_assets(self, request, *args, **kwargs): "middleware_hostname": asset.current_location.facility.middleware_address, } ) - asset_class.validate_action(action) - result = asset_class.handle_action(action) + result = asset_class.handle_action( + action, + { + "username": request.user.username, + "asset_id": asset.external_id, + }, + ) return Response({"result": result}, status=status.HTTP_200_OK) + + except PermissionDenied as e: + return Response( + { + "message": e.detail.get("message", None), + "username": e.detail.get("username", None), + "firstName": e.detail.get("firstName", None), + "lastName": e.detail.get("lastName", None), + "role": e.detail.get("role", None), + "homeFacility": e.detail.get("homeFacility", None), + }, + status=status.HTTP_409_CONFLICT, + ) except ValidationError as e: return Response({"message": e.detail}, status=status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/models/notification.py b/care/facility/models/notification.py index de2b1d59d8..f2de9a78ec 100644 --- a/care/facility/models/notification.py +++ b/care/facility/models/notification.py @@ -23,6 +23,7 @@ class Medium(enum.Enum): class Event(enum.Enum): MESSAGE = 0 + ASSET_UNLOCKED = 10 PATIENT_CREATED = 20 PATIENT_UPDATED = 30 PATIENT_DELETED = 40 diff --git a/care/facility/tests/test_asset_operate_api.py b/care/facility/tests/test_asset_operate_api.py index ab9fe0410a..f944c59657 100644 --- a/care/facility/tests/test_asset_operate_api.py +++ b/care/facility/tests/test_asset_operate_api.py @@ -2,7 +2,7 @@ from rest_framework.test import APIRequestFactory, APITestCase from care.facility.api.viewsets.asset import AssetViewSet -from care.facility.models import Asset, AssetBed, AssetLocation, Bed +from care.facility.models import Asset, AssetBed, AssetLocation, Bed, FacilityUser from care.facility.tests.mixins import TestClassMixin from care.utils.tests.test_base import TestBase @@ -15,7 +15,11 @@ def setUp(self): state = self.create_state() district = self.create_district(state=state) self.user = self.create_user(district=district, username="test user") + self.user_2 = self.create_user(district=district, username="test user 2") facility = self.create_facility(district=district, user=self.user) + self.facility_user = FacilityUser.objects.create( + user=self.user_2, facility=facility, created_by=self.user + ) self.asset1_location = AssetLocation.objects.create( name="asset1 location", location_type=1, facility=facility ) @@ -124,3 +128,73 @@ def test_ventilator(self): self.asset.meta = self.ventilator_meta self.asset.save() pass + + def test_lock_asset(self): + self.asset.asset_class = "ONVIF" + self.asset.meta = self.onvif_meta + self.asset.save() + sample_data_lock = { + "action": { + "type": "lock_asset", + "data": {}, + } + } + sample_data_unlock = { + "action": { + "type": "unlock_asset", + "data": {}, + } + } + response = self.new_request( + ( + f"/api/v1/asset/{self.asset.external_id}/operate_assets/", + sample_data_lock, + "json", + ), + {"post": "operate_assets"}, + AssetViewSet, + self.user, + {"external_id": self.asset.external_id}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.new_request( + ( + f"/api/v1/asset/{self.asset.external_id}/operate_assets/", + sample_data_unlock, + "json", + ), + {"post": "operate_assets"}, + AssetViewSet, + self.user_2, + {"external_id": self.asset.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + + response = self.new_request( + ( + f"/api/v1/asset/{self.asset.external_id}/operate_assets/", + sample_data_unlock, + "json", + ), + {"post": "operate_assets"}, + AssetViewSet, + self.user, + {"external_id": self.asset.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.new_request( + ( + f"/api/v1/asset/{self.asset.external_id}/operate_assets/", + sample_data_lock, + "json", + ), + {"post": "operate_assets"}, + AssetViewSet, + self.user_2, + {"external_id": self.asset.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.asset.delete() diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 40f91d0d70..87f7a0759d 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -1,15 +1,23 @@ +import enum import json import requests from django.conf import settings -from rest_framework.exceptions import APIException +from django.core.cache import cache +from rest_framework.exceptions import APIException, PermissionDenied +from care.users.models import User from care.utils.jwks.token_generator import generate_jwt class BaseAssetIntegration: auth_header_type = "Care_Bearer " + class BaseAssetActions(enum.Enum): + UNLOCK_ASSET = "unlock_asset" + LOCK_ASSET = "lock_asset" + REQUEST_ACCESS = "request_access" + def __init__(self, meta): self.meta = meta self.host = self.meta["local_ip_address"] @@ -55,3 +63,116 @@ def api_get(self, url, data=None): def validate_action(self, action): pass + + def generate_system_users(self, asset_id): + asset_queue_key = f"waiting_queue_{asset_id}" + if cache.get(asset_queue_key) is None: + return [] + else: + queue = cache.get(asset_queue_key) + users_array = [] + for user in queue: + users_array.append(User.objects.get(username=user)) + return users_array + + def generate_notification(self, asset_id): + from care.utils.notification_handler import send_webpush + + message = { + "type": "MESSAGE", + "asset_id": str(asset_id), + "status": "success", + } + user_array = self.generate_system_users(asset_id) + for username in user_array: + send_webpush(username=username, message=json.dumps(message)) + + def add_to_waiting_queue(self, username, asset_id): + asset_queue_key = f"waiting_queue_{asset_id}" + if cache.get(asset_queue_key) is None: + cache.set(asset_queue_key, [username], timeout=None) + else: + queue = cache.get(asset_queue_key) + if username not in queue: + queue.append(username) + cache.set(asset_queue_key, queue, timeout=None) + + def remove_from_waiting_queue(self, username, asset_id): + asset_queue_key = f"waiting_queue_{asset_id}" + if cache.get(asset_queue_key) is None: + return + else: + queue = cache.get(asset_queue_key) + if username in queue: + queue = [x for x in queue if x != username] + cache.set(asset_queue_key, queue, timeout=None) + + def unlock_asset(self, username, asset_id): + if cache.get(asset_id) is None: + self.remove_from_waiting_queue(username, asset_id) + self.generate_notification(asset_id) + return True + elif cache.get(asset_id) == username: + cache.delete(asset_id) + self.remove_from_waiting_queue(username, asset_id) + self.generate_notification(asset_id) + return True + elif cache.get(asset_id) != username: + self.remove_from_waiting_queue(username, asset_id) + return False + return True + + def lock_asset(self, username, asset_id): + if cache.get(asset_id) is None or not cache.get(asset_id): + cache.set(asset_id, username, timeout=None) + self.remove_from_waiting_queue(username, asset_id) + return True + elif cache.get(asset_id) == username: + self.remove_from_waiting_queue(username, asset_id) + return True + self.add_to_waiting_queue(username, asset_id) + return False + + def raise_conflict(self, asset_id): + user: User = User.objects.get(username=cache.get(asset_id)) + raise PermissionDenied( + { + "message": "Asset is currently in use by another user", + "username": user.username, + "firstName": user.first_name, + "lastName": user.last_name, + "role": [x for x in User.TYPE_CHOICES if x[0] == user.user_type][0][1], + "homeFacility": user.home_facility.name + if (user.home_facility and user.home_facility.name) + else "", + } + ) + + def verify_access(self, username, asset_id): + if cache.get(asset_id) is None or cache.get(asset_id) == username: + return True + elif cache.get(asset_id) != username: + return False + return True + + def request_access(self, username, asset_id): + from care.utils.notification_handler import send_webpush + + if cache.get(asset_id) is None or cache.get(asset_id) == username: + return {} + elif cache.get(asset_id) != username: + user: User = User.objects.get(username=username) + message = { + "type": "MESSAGE", + "status": "request", + "username": user.username, + "firstName": user.first_name, + "lastName": user.last_name, + "role": [x for x in User.TYPE_CHOICES if x[0] == user.user_type][0][1], + "homeFacility": user.home_facility.name + if (user.home_facility and user.home_facility.name) + else "", + } + + send_webpush(username=cache.get(asset_id), message=json.dumps(message)) + return {"message": "user notified"} diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index f7f1e7c783..91896f207f 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -19,7 +19,7 @@ def __init__(self, meta): dict((key, f"{key} not found in asset metadata") for key in e.args) ) - def handle_action(self, action): + def handle_action(self, action, verifcation_data: dict = None): action_type = action["type"] if action_type == self.HL7MonitorActions.GET_VITALS.value: diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 4fcad24905..d23585e216 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -1,5 +1,6 @@ import enum +from django.core.cache import cache from rest_framework.exceptions import ValidationError from care.utils.assetintegration.base import BaseAssetIntegration @@ -26,13 +27,16 @@ def __init__(self, meta): dict((key, f"{key} not found in asset metadata") for key in e.args) ) - def handle_action(self, action): + def handle_action(self, action, verifcation_data: dict = None): action_type = action["type"] action_data = action.get("data", {}) allowed_action_data = ["x", "y", "zoom"] action_data = { key: action_data[key] for key in action_data if key in allowed_action_data } + + username = verifcation_data.get("username", None) + asset_id = verifcation_data.get("asset_id", None) request_body = { "hostname": self.host, "port": 80, @@ -42,6 +46,21 @@ def handle_action(self, action): **action_data, } + print(cache.get(f"waiting_queue_{asset_id}")) + + if action_type == BaseAssetIntegration.BaseAssetActions.REQUEST_ACCESS.value: + return self.request_access(username, asset_id) + + if action_type == BaseAssetIntegration.BaseAssetActions.UNLOCK_ASSET.value: + if self.unlock_asset(username, asset_id): + return {"message": "Asset Unlocked"} + self.raise_conflict(asset_id=asset_id) + + if action_type == BaseAssetIntegration.BaseAssetActions.LOCK_ASSET.value: + if self.lock_asset(username, asset_id): + return {"message": "Asset Locked"} + self.raise_conflict(asset_id=asset_id) + if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: return self.api_get(self.get_url("status"), request_body) @@ -49,13 +68,19 @@ def handle_action(self, action): return self.api_get(self.get_url("presets"), request_body) if action_type == self.OnvifActions.GOTO_PRESET.value: - return self.api_post(self.get_url("gotoPreset"), request_body) + if self.verify_access(username, asset_id): + return self.api_post(self.get_url("gotoPreset"), request_body) + self.raise_conflict(asset_id=asset_id) if action_type == self.OnvifActions.ABSOLUTE_MOVE.value: - return self.api_post(self.get_url("absoluteMove"), request_body) + if self.verify_access(username, asset_id): + return self.api_post(self.get_url("absoluteMove"), request_body) + self.raise_conflict(asset_id=asset_id) if action_type == self.OnvifActions.RELATIVE_MOVE.value: - return self.api_post(self.get_url("relativeMove"), request_body) + if self.verify_access(username, asset_id): + return self.api_post(self.get_url("relativeMove"), request_body) + self.raise_conflict(asset_id=asset_id) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index 10af39b50f..c348715578 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -19,7 +19,7 @@ def __init__(self, meta): dict((key, f"{key} not found in asset metadata") for key in e.args) ) - def handle_action(self, action): + def handle_action(self, action, verifcation_data: dict = None): action_type = action["type"] if action_type == self.VentilatorActions.GET_VITALS.value: diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index 677eb132d4..36ddb2ec87 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -5,6 +5,7 @@ from django.conf import settings from pywebpush import WebPushException, webpush +from care.facility.models.asset import Asset from care.facility.models.daily_round import DailyRound from care.facility.models.facility import Facility, FacilityUser from care.facility.models.notification import Notification @@ -230,6 +231,11 @@ def generate_system_message(self): self.caused_object.patient.name, self.caused_by.get_full_name(), ) + elif isinstance(self.caused_object, Asset): + if self.event == Notification.Event.ASSET_UNLOCKED.value: + message = "{} is ready to use".format( + self.caused_object.name, + ) return message def generate_sms_message(self):