391 lines
17 KiB
Python
391 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
'''
|
|
biomon - Signs monitoring and patient management application
|
|
|
|
Copyright (C) 2015 Entr'ouvert
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
|
|
import urllib
|
|
import urllib2
|
|
import urlparse
|
|
import json
|
|
import copy
|
|
import dateutil.parser
|
|
from datetime import datetime
|
|
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
from django.http import Http404, HttpResponse
|
|
from django.conf import settings
|
|
|
|
import matplotlib
|
|
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
|
|
from .. import cbv
|
|
from .. import whisper_backend
|
|
from .. import models
|
|
from . import graphite_custom
|
|
from ..medibot import models as medibot_models
|
|
from ..medibot import definitions as definition_utils
|
|
|
|
|
|
CRITICAL_COLOR = "FF0000"
|
|
DANGEROUS_COLOR = "FFA500"
|
|
#NORMAL_COLOR = "008000"
|
|
NORMAL_COLOR = "B0B0B0"
|
|
|
|
def get_int_parameter(request, name, default):
|
|
if name in request.GET:
|
|
try:
|
|
return int(request.GET.get(name))
|
|
except:
|
|
pass
|
|
return default
|
|
|
|
def get_isodate_parameter(request, name, default):
|
|
if name in request.GET:
|
|
try:
|
|
return dateutil.parser.parse(request.GET.get(name))
|
|
except:
|
|
pass
|
|
return default
|
|
|
|
|
|
class RenderView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
metric = request.GET.get("metric")
|
|
if not metric:
|
|
raise Http404('No metric specified')
|
|
|
|
# Shared ref on a mutable, prevent error when the view is called multiple times in parallel
|
|
qs_dic = copy.deepcopy(graphite_custom.RENDER_PARAMS)
|
|
until = get_int_parameter(request, 'until', None)
|
|
duration = get_int_parameter(request, 'duration', 0)
|
|
if metric == settings.WHISPER_TEMPERATURE_METRIC:
|
|
qs_dic["vtitle"] = "Celsius"
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_TEMPERATURE_METRIC)
|
|
margin = 2
|
|
else:
|
|
qs_dic["vtitle"] = "BPM"
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_HEARTRATE_METRIC)
|
|
margin = 20
|
|
qs_dic["yMin"] = backend.get_min(duration=duration, until=until) - margin
|
|
qs_dic["yMax"] = backend.get_max(duration=duration, until=until) + margin
|
|
metric_fp = '.'.join([settings.WHISPER_METRIC_PATH, subject, metric])
|
|
target_l = [metric_fp]
|
|
mapping = {v: k for k, v in settings.SENSOR_MAPPING.items()}
|
|
color = NORMAL_COLOR
|
|
""" Chaging metric line color when there a running alert is not
|
|
very clean for the user, especially because the time frame may be
|
|
wider than the episode duration and then the color be not appropriate
|
|
at some time.
|
|
if medibot_models.Episode.objects.filter(patient=self.get_object(), level='CRITICAL', opened=True, metric=mapping[metric]).exists():
|
|
color = CRITICAL_COLOR
|
|
elif medibot_models.Episode.objects.filter(patient=self.get_object(), level='DANGEROUS', opened=True, metric=mapping[metric]).exists():
|
|
color = DANGEROUS_COLOR"""
|
|
color_l = [color]
|
|
|
|
qs = urlparse.parse_qs(request.META.get('QUERY_STRING', ''))
|
|
for key in ("until", "from", "fresh"):
|
|
qs_dic[key] = request.GET.get(key, None)
|
|
|
|
sap = self.get_object().get_simple_alert_profile()
|
|
if sap:
|
|
prefix = ''
|
|
if metric == settings.WHISPER_TEMPERATURE_METRIC:
|
|
prefix = 't'
|
|
else:
|
|
prefix = 'hr'
|
|
value = sap.get(prefix + '_max_critical', None)
|
|
if value and value < qs_dic["yMax"]:
|
|
target_l.append("constantLine(%s)" % value)
|
|
color_l.append(CRITICAL_COLOR)
|
|
value = sap.get(prefix + '_min_critical', None)
|
|
if value and value > qs_dic["yMin"]:
|
|
target_l.append("constantLine(%s)" % value)
|
|
color_l.append(CRITICAL_COLOR)
|
|
value = sap.get(prefix + '_max_dangerous', None)
|
|
if value and value < qs_dic["yMax"]:
|
|
target_l.append("constantLine(%s)" % value)
|
|
color_l.append(DANGEROUS_COLOR)
|
|
value = sap.get(prefix + '_min_dangerous', None)
|
|
if value and value > qs_dic["yMin"]:
|
|
target_l.append("constantLine(%s)" % value)
|
|
color_l.append(DANGEROUS_COLOR)
|
|
if getattr(settings, 'GRAPHITE_HOST', False):
|
|
qs = urllib.urlencode(qs_dic)
|
|
targets = '&'.join(['='.join(('target', value)) for value in target_l])
|
|
qs += '&' + targets
|
|
qs += '&colorList=' + ','.join(color_l)
|
|
url = "{0}{1}?{2}".format(settings.GRAPHITE_HOST, "/render/", qs)
|
|
rendering = urllib2.urlopen(url).read()
|
|
if rendering:
|
|
return HttpResponse(rendering, content_type='image/png')
|
|
raise Http404('Unable to fetch rendering')
|
|
raise Http404('No graphite host specified')
|
|
|
|
|
|
class InstantsView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
until = get_int_parameter(request, 'until', None)
|
|
until_dt = datetime.now()
|
|
if until:
|
|
until_dt = datetime.fromtimestamp(until)
|
|
values = list()
|
|
mapping = {v: k for k, v in settings.SENSOR_MAPPING.items()}
|
|
def process(metric, values, lookup_range=60):
|
|
backend = whisper_backend.WhisperBackend(subject, metric)
|
|
values.append(backend.get_last_value(until=until, lookup_range=lookup_range))
|
|
level = ""
|
|
if medibot_models.Episode.objects.filter(patient=self.get_object(), level='CRITICAL', opened=True, metric=mapping[metric], start__lte=until_dt).exists() or medibot_models.Episode.objects.filter(patient=self.get_object(), level='CRITICAL', opened=False, metric=mapping[metric], start__lte=until_dt, end__gte=until_dt).exists():
|
|
level = "critical"
|
|
elif medibot_models.Episode.objects.filter(patient=self.get_object(), level='DANGEROUS', opened=True, metric=mapping[metric], start__lte=until_dt).exists() or medibot_models.Episode.objects.filter(patient=self.get_object(), level='DANGEROUS', opened=False, metric=mapping[metric], start__lte=until_dt, end__gte=until_dt).exists():
|
|
level = "dangerous"
|
|
values.append(level)
|
|
metric = settings.WHISPER_HEARTRATE_METRIC
|
|
process(metric, values)
|
|
metric = settings.WHISPER_TEMPERATURE_METRIC
|
|
process(metric, values, lookup_range=300)
|
|
return HttpResponse(json.dumps(values), content_type='application/json')
|
|
|
|
|
|
class MeansView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
until = get_int_parameter(request, 'until', None)
|
|
duration = get_int_parameter(request, 'duration', 0)
|
|
values = list()
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_HEARTRATE_METRIC)
|
|
values.append(backend.get_mean(duration=duration, until=until))
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_TEMPERATURE_METRIC)
|
|
values.append(backend.get_mean(duration=duration, until=until))
|
|
return HttpResponse(json.dumps(values), content_type='application/json')
|
|
|
|
|
|
def get_ax():
|
|
fig = plt.figure()
|
|
fig.patch.set_facecolor('black')
|
|
ax = fig.add_subplot(111, axisbg='black')
|
|
ax.spines['bottom'].set_color('#26B3F7')
|
|
ax.spines['top'].set_color('#26B3F7')
|
|
ax.spines['left'].set_color('#26B3F7')
|
|
ax.spines['right'].set_color('#26B3F7')
|
|
ax.xaxis.label.set_color('#26B3F7')
|
|
ax.tick_params(axis='x', colors='#26B3F7')
|
|
ax.yaxis.label.set_color('#26B3F7')
|
|
ax.tick_params(axis='y', colors='#26B3F7')
|
|
ax.set_ylabel('Frequence')
|
|
ax.grid(True, color='white')
|
|
return fig, ax
|
|
|
|
|
|
class HistogramView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
metric = request.GET.get("metric")
|
|
_from = get_int_parameter(request, 'from', None)
|
|
until = get_int_parameter(request, 'until', None)
|
|
if not _from or not until or not metric:
|
|
raise Http404('Missing parameter')
|
|
backend = whisper_backend.WhisperBackend(subject, metric)
|
|
data = backend.get_data(_from, until)
|
|
if not data or len(data) < 2:
|
|
raise Http404('No data')
|
|
fig, ax = get_ax()
|
|
if metric == settings.WHISPER_HEARTRATE_METRIC:
|
|
bins = np.arange(min(data), max(data) + 10, 5)
|
|
else:
|
|
bins = np.arange(min(data), max(data) + 1, 0.5)
|
|
ax.hist(data, bins=bins, color='#B0B0B0', align='left')
|
|
ax.set_xlabel(metric)
|
|
ax.set_xticks(bins[:-1])
|
|
ax.axvline(sum(data)/len(data), color='w', linestyle='dashed', linewidth=5)
|
|
canvas = FigureCanvasAgg(fig)
|
|
response = HttpResponse(content_type='image/png')
|
|
canvas.print_png(response)
|
|
matplotlib.pyplot.close(fig)
|
|
return response
|
|
|
|
|
|
class MinMaxView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
until = get_int_parameter(request, 'until', None)
|
|
duration = get_int_parameter(request, 'duration', 0)
|
|
values = list()
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_HEARTRATE_METRIC)
|
|
values.append(backend.get_min(duration=duration, until=until))
|
|
values.append(backend.get_max(duration=duration, until=until))
|
|
backend = whisper_backend.WhisperBackend(subject, settings.WHISPER_TEMPERATURE_METRIC)
|
|
values.append(backend.get_min(duration=duration, until=until))
|
|
values.append(backend.get_max(duration=duration, until=until))
|
|
return HttpResponse(json.dumps(values), content_type='application/json')
|
|
|
|
|
|
class PatientEpisodesOpened(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
values = [(episode.seq_id, episode.start.isoformat(),
|
|
episode.end.isoformat(), json.loads(episode.definition),
|
|
episode.duration, episode.start.date().isoformat(),
|
|
episode.checked, episode.id)
|
|
for episode in medibot_models.Episode.objects.
|
|
filter(patient=self.get_object(),
|
|
opened=True).order_by('-start')]
|
|
return HttpResponse(json.dumps(values),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientEpisodesClosed(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
after = get_isodate_parameter(request, 'after',
|
|
datetime.fromtimestamp(0))
|
|
values = [(episode.seq_id, episode.start.isoformat(),
|
|
episode.end.isoformat(), json.loads(episode.definition),
|
|
episode.duration, episode.end.date().isoformat(),
|
|
episode.checked, episode.id)
|
|
for episode in medibot_models.Episode.objects.
|
|
filter(patient=self.get_object(),
|
|
opened=False, end__gt=after).order_by('-end')]
|
|
return HttpResponse(json.dumps(values),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientNextEpisode(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
after = get_int_parameter(request, 'after', None)
|
|
after_dt = datetime.fromtimestamp(after)
|
|
nexts = medibot_models.Episode.objects.filter(patient=self.get_object(), start__gt=after_dt).order_by('start')
|
|
next = []
|
|
if nexts:
|
|
episode = nexts[0]
|
|
next = [episode.id, episode.seq_id,
|
|
json.loads(episode.definition), episode.checked,
|
|
episode.start.isoformat()]
|
|
if episode.end:
|
|
next.append(episode.end.isoformat())
|
|
return HttpResponse(json.dumps(next),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientPreviousEpisode(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
before = get_int_parameter(request, 'before', None)
|
|
before_dt = datetime.fromtimestamp(before)
|
|
nexts = medibot_models.Episode.objects.filter(patient=self.get_object(), start__lt=before_dt).order_by('-start')
|
|
next = []
|
|
if nexts:
|
|
episode = nexts[0]
|
|
next = [episode.id, episode.seq_id,
|
|
json.loads(episode.definition), episode.checked,
|
|
episode.start.isoformat()]
|
|
if episode.end:
|
|
next.append(episode.end.isoformat())
|
|
return HttpResponse(json.dumps(next),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientEpisodesStatsView(cbv.BasicDetailView):
|
|
model = models.Patient
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
subject = str(self.get_object().id)
|
|
criticals = medibot_models.Episode.objects.filter(patient=self.get_object(), level='CRITICAL', checked=False)
|
|
criticals_count = criticals.count()
|
|
criticals_active = criticals.filter(opened=True).exists()
|
|
dangerous = medibot_models.Episode.objects.filter(patient=self.get_object(), level='DANGEROUS', checked=False)
|
|
dangerous_count = dangerous.count()
|
|
dangerous_active = dangerous.filter(opened=True).exists()
|
|
return HttpResponse(json.dumps([criticals_count, criticals_active, dangerous_count, dangerous_active]), content_type='application/json')
|
|
|
|
|
|
class PatientsEpisodesOpened(cbv.BasicView):
|
|
def get(self, request, *args, **kwargs):
|
|
values = [(episode.seq_id, episode.start.isoformat(),
|
|
episode.end.isoformat(), json.loads(episode.definition),
|
|
episode.duration, episode.start.date().isoformat(),
|
|
episode.checked, episode.id,
|
|
episode.patient.id, episode.patient.display_name,
|
|
episode.patient.monitoring_place)
|
|
for episode in medibot_models.Episode.objects.
|
|
filter(opened=True, checked=False).order_by('-start')]
|
|
return HttpResponse(json.dumps(values),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientsEpisodesClosed(cbv.BasicView):
|
|
def get(self, request, *args, **kwargs):
|
|
after = get_isodate_parameter(request, 'after',
|
|
datetime.fromtimestamp(0))
|
|
values = [(episode.seq_id, episode.start.isoformat(),
|
|
episode.end.isoformat(), json.loads(episode.definition),
|
|
episode.duration, episode.end.date().isoformat(),
|
|
episode.checked, episode.id,
|
|
episode.patient.id, episode.patient.display_name,
|
|
episode.patient.monitoring_place)
|
|
for episode in medibot_models.Episode.objects.
|
|
filter(opened=False, checked=False, end__gt=after).order_by('-end')]
|
|
return HttpResponse(json.dumps(values),
|
|
content_type='application/json')
|
|
|
|
|
|
class PatientsEpisodesStatsView(cbv.BasicView):
|
|
def get(self, request, *args, **kwargs):
|
|
criticals = medibot_models.Episode.objects.filter(level='CRITICAL', checked=False)
|
|
criticals_count = criticals.count()
|
|
criticals_active = criticals.filter(opened=True).exists()
|
|
dangerous = medibot_models.Episode.objects.filter(level='DANGEROUS', checked=False)
|
|
dangerous_count = dangerous.count()
|
|
dangerous_active = dangerous.filter(opened=True).exists()
|
|
return HttpResponse(json.dumps([criticals_count, criticals_active, dangerous_count, dangerous_active]), content_type='application/json')
|
|
|
|
class PatientsEpisodesNotChecked(cbv.BasicView):
|
|
def get(self, request, *args, **kwargs):
|
|
after = get_isodate_parameter(request, 'after',
|
|
datetime.fromtimestamp(0))
|
|
values = [(episode.seq_id, episode.start.isoformat(),
|
|
episode.end.isoformat(), json.loads(episode.definition),
|
|
episode.duration, episode.end.date().isoformat(),
|
|
episode.checked, episode.id,
|
|
episode.patient.id, episode.patient.display_name,
|
|
episode.patient.monitoring_place)
|
|
for episode in medibot_models.Episode.objects.
|
|
filter(checked=False).order_by('-end')]
|
|
return HttpResponse(json.dumps(values),
|
|
content_type='application/json')
|