merge experimental #1

Merged
jlobbes merged 25 commits from experimental into master 2023-08-17 15:16:51 +01:00
14 changed files with 418 additions and 41 deletions
Showing only changes of commit 84534b51f2 - Show all commits

View File

@ -1,35 +1,58 @@
{% extends 'core/base.html' %} {% extends 'core/base.html' %}
{% load static %}
{% load widget_tweaks %}
{% block title %}
Medwings | Log In
{% endblock title %}
{% block content %} {% block content %}
<div class="flex flex-col justify-center items-center gap-2 py-4"> <div class="flex flex-col justify-center items-center gap-2 py-4">
{% if form.errors %} <h1>Log In</h1>
<div class="status-message error">
<p>Your username and password didn't match. Please try again.</p>
</div>
{% endif %}
{% if next %}
<div class="status-message error">
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please <a href="{% url 'login' %}">log in</a> to see this page.</p>
{% endif %}
</div>
{% endif %}
<form method="post" action="{% url 'login' %}"> <form method="post" action="{% url 'login' %}">
{% csrf_token %} {% csrf_token %}
<fieldset class="border border-accent p-4"> <fieldset class="flex flex-col gap-4 items-center max-w-sm">
<legend>Please enter your login details</legend> <legend>Please enter your login details</legend>
<div class="grid grid-cols-4 justify-center items-center gap-2">
<div class="col-span-1">{{ form.username.label_tag }}</div> {% if form.non_field_errors %}
<div class="col-span-3">{{ form.username }}</div> <div class="flex flex-col gap-2 status-message error">
<div class="col-span-1">{{ form.password.label_tag }}</div> {% for error in form.non_field_errors %}
<div class="col-span-3">{{ form.password }}</div> <p class="error">{{ error }}</p>
<input class="col-span-4" type="submit" value="Log In"> {% endfor %}
</div>
{% endif %}
<div class="flex flex-col gap-8">
<div class="flex flex-col">
{% render_field form.username|add_error_class:"error" %}
<label class="text-sm text-accent-800 font-semibold" for="{{ form.username.id_for_label }}">
{% render_field form.username.label %}
</label>
{% if form.username.errors %}
<div class="flex flex-col gap-2 status-message error">
{% for error in form.username.errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="flex flex-col">
{% render_field form.password|add_error_class:"error" %}
<label class="text-sm text-accent-800 font-semibold" for="{{ form.password.id_for_label }}">
{% render_field form.password.label %}
</label>
{% if form.password.errors %}
<div class="flex flex-col gap-2 status-message error">
{% for error in form.password.errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div> </div>
<input type="hidden" name="next" value="{{ next }}">
<input class="max-w-64" type="submit" value="Log In">
</fieldset> </fieldset>
</form> </form>

View File

@ -29,6 +29,7 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'widget_tweaks',
'core', 'core',
'authentication', 'authentication',
'medwings', 'medwings',

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-29 18:28 # Generated by Django 4.2.3 on 2023-07-30 21:15
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from medwings.models import Profile from medwings.models import Profile, RespirationScoreRecord
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
@ -10,3 +10,9 @@ class ProfileForm(forms.ModelForm):
widgets = { widgets = {
'date_of_birth': forms.DateInput(attrs={'type': 'date'}), 'date_of_birth': forms.DateInput(attrs={'type': 'date'}),
} }
class RespirationScoreForm(forms.ModelForm):
class Meta:
model = RespirationScoreRecord
fields = ['value_severity']

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-29 18:28 # Generated by Django 4.2.3 on 2023-07-30 21:15
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -79,7 +79,8 @@ class Migration(migrations.Migration):
('blood_pressure_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bloodpressurerecord')), ('blood_pressure_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bloodpressurerecord')),
('body_temp_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bodytemprecord')), ('body_temp_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bodytemprecord')),
('heart_rate_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.heartraterecord')), ('heart_rate_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.heartraterecord')),
('respiration_score_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.spo2levelrecord')), ('respiration_score_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.respirationscorerecord')),
('spo2_level_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.spo2levelrecord')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),

View File

@ -119,3 +119,5 @@ class MewsRecord(models.Model):
mews_value += 0 mews_value += 0
else: else:
mews_value += 2 mews_value += 2
return mews_value

View File

@ -0,0 +1,191 @@
{% extends 'core/base.html' %}
{% load static %}
{% load widget_tweaks %}
{% block title %}
Medwings | Take a measurement
{% endblock title %}
{% block content %}
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
<h1>Record your health status</h1>
<div id="pageContainer" class="grid grid-cols-3 gap-6 text-xl justify-center items-center w-full p-4 border rounded-md">
<div class="font-semibold text-center sm:text-start col-span-2">
<p>Blood Pressure (systolic)</p>
</div>
<div id="bloodPressureLoader" class="loader h-1 bg-accent loader--loading"></div>
<div id="bloodPressureValue" class="font-bold text-end hidden">
<p><span id="bloodPressureValueNumber"></span> mmHG</p>
</div>
<div class="col-span-3 h-px bg-secondary"></div>
<div class="font-semibold text-center sm:text-start col-span-2">
<p>Body Temperature</p>
</div>
<div id="bodyTempLoader" class="loader h-1 bg-accent loader--loading"></div>
<div id="bodyTempValue" class="font-bold text-end hidden">
<p><span id="bodyTempValueNumber"></span> °C</p>
</div>
<div class="col-span-3 h-px bg-secondary"></div>
<div class="font-semibold text-center sm:text-start col-span-2">
<p>Heart Rate</p>
</div>
<div id="heartRateLoader" class="loader h-1 bg-accent loader--loading"></div>
<div id="heartRateValue" class="font-bold text-end hidden">
<p><span id="heartRateValueNumber"></span> bpm</p>
</div>
<div class="col-span-3 h-px bg-secondary"></div>
<div class="font-semibold text-center sm:text-start col-span-2">
<p>Blood Oxygenation</p>
</div>
<div id="spo2Loader" class="loader h-1 bg-accent loader--loading"></div>
<div id="spo2Value" class="font-bold text-end hidden">
<p><span id="spo2ValueNumber"></span> %</p>
</div>
<div class="col-span-3 h-px bg-secondary"></div>
<div class="font-semibold text-center sm:text-start col-span-2">
<p>Respiration Score</p>
</div>
<div id="respirationScoreLoader" class="loader h-1 bg-accent loader--loading"></div>
<div id="respirationScoreValue" class="font-bold text-end hidden">
<p><span id="respirationScoreValueNumber"></span></p>
</div>
<div class="col-span-3 h-0.5 bg-secondary"></div>
<div class="font-semibold text-center sm:text-start col-span-2">
<p>MEWS</p>
</div>
<div id="mewsLoader" class="loader h-1 bg-accent loader--loading"></div>
<div id="mewsValue" class="font-bold text-end hidden">
<p><span id="mewsValueNumber"></span></p>
</div>
</div>
</div>
<script>
const elements = {
"bloodPressure": {
"loader": document.getElementById("bloodPressureLoader"),
"value": document.getElementById("bloodPressureValue"),
"number": document.getElementById("bloodPressureValueNumber")
},
"bodyTemp": {
"loader": document.getElementById("bodyTempLoader"),
"value": document.getElementById("bodyTempValue"),
"number": document.getElementById("bodyTempValueNumber")
},
"heartRate": {
"loader": document.getElementById("heartRateLoader"),
"value": document.getElementById("heartRateValue"),
"number": document.getElementById("heartRateValueNumber")
},
"spo2": {
"loader": document.getElementById("spo2Loader"),
"value": document.getElementById("spo2Value"),
"number": document.getElementById("spo2ValueNumber")
},
"respirationScore": {
"loader": document.getElementById("respirationScoreLoader"),
"value": document.getElementById("respirationScoreValue"),
"number": document.getElementById("respirationScoreValueNumber")
},
"mews": {
"loader": document.getElementById("mewsLoader"),
"value": document.getElementById("mewsValue"),
"number": document.getElementById("mewsValueNumber")
},
}
function showError(message) {
let pageContainerDiv = document.getElementById("pageContainer");
pageContainerDiv.innerHTML = `<div class="col-span-3 flex justify-center items-center status-message error"><p>${message}</p></div>`
}
const pageTimeout = 10 * 60 * 1000;
setTimeout(() => {
if (!fetchingComplete)
showError('Your measurement timed out. Please <a class="underline" href="/mews/init/">start again</a>.')
}, pageTimeout);
let fetchingData = false;
let fetchingComplete = false;
const pollingInterval = 5000;
let currentData = {
"blood_pressure_value": null,
"body_temp_value": null,
"heart_rate_value": null,
"spo2_level_value": null,
"respiration_score_value": null,
"mews_value": null
};
async function fetchData() {
if (fetchingData || fetchingComplete) return;
fetchingFata = true;
fetch("{% url 'mews-status' %}")
.then(response => {
if (!response.ok) {
throw new Error('There was an error while retrieving your data.');
}
return response.json();
})
.then(data => {
const names = {
"blood_pressure_value": "bloodPressure",
"body_temp_value": "bodyTemp",
"heart_rate_value": "heartRate",
"spo2_level_value": "spo2",
"respiration_score_value": "respirationScore",
"mews_value": "mews"
}
for (let type of Object.keys(names)) {
if (currentData[type] === null && data[type] !== null) {
currentData[type] = data[type];
setTimeout(() => {
loadValue(names[type], data[type])
}, Math.floor(Math.random() * (1500 - 500 + 1)) + 500);
}
}
if (Object.values(currentData).every(value => value !== null)) {
fetchingComplete = true;
}
fetchingData = false;
if (!fetchingComplete) {
setTimeout(() => {
fetchData();
}, pollingInterval)
}
})
.catch(error => {
showError(error)
});
}
fetchData();
function loadValue(element, value) {
const loaderDiv = elements[element]["loader"];
const valueDiv = elements[element]["value"];
const numberSpan = elements[element]["number"];
loaderDiv.classList.remove("loader--loading");
loaderDiv.classList.add("loader--finished");
setTimeout(function() {
loaderDiv.classList.remove("loader--finished");
loaderDiv.classList.add("hidden");
valueDiv.classList.remove('hidden');
valueDiv.classList.add('loader--value');
numberSpan.textContent = value;
}, 2000);
}
</script>
{% endblock content %}

View File

@ -0,0 +1,43 @@
{% extends 'core/base.html' %}
{% load static %}
{% load widget_tweaks %}
{% block title %}
Medwings | Take a measurement
{% endblock title %}
{% block content %}
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
<h1>Record your health status</h1>
<form method="post">
{% csrf_token %}
<fieldset class="flex flex-col gap-4 items-center">
<legend>To get started, please answer the following question:</legend>
{% if respiration_score_form.non_field_errors %}
<div class="flex flex-col gap-2 status-message error">
{% for error in respiration_score_form.non_field_errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="flex flex-col sm:flex-row gap-x-2">
<label class="font-semibold" for="{{ respiration_score_form.value_severity.id_for_label }}">
Are you experiencing any shortness of breath?
</label>
{% render_field respiration_score_form.value_severity|add_error_class:"error" %}
</div>
{% if respiration_score_form.value_severity.errors %}
<div class="flex flex-col gap-2 status-message error">
{% for error in respiration_score_form.value_severity.errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<button class="btn" type="submit">Continue</button>
</fieldset>
</form>
</div>
{% endblock content %}

View File

@ -5,5 +5,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="home"), path("", views.index, name="home"),
path("dashboard/", views.dashboard, name="dashboard"), path("dashboard/", views.dashboard, name="dashboard"),
path("mews/init/", views.mews_init, name="mews-init"),
path("mews/continue/", views.mews_continue, name="mews-continue"),
path("mews/status/", views.mews_status, name="mews-status"), path("mews/status/", views.mews_status, name="mews-status"),
] ]

View File

@ -1,12 +1,13 @@
from datetime import timedelta from datetime import timedelta
from django.shortcuts import render from django.shortcuts import redirect, render
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.utils import timezone from django.utils import timezone
from . import models from . import models
from . import forms
@require_http_methods(["GET"]) @require_http_methods(["GET"])
@ -14,10 +15,41 @@ def index(request):
return render(request, 'medwings/index.html') return render(request, 'medwings/index.html')
@login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def dashboard(request): def dashboard(request):
return render(request, 'medwings/dashboard.html') return render(request, 'medwings/dashboard.html')
@login_required
@require_http_methods(["GET", "POST"])
def mews_init(request):
if request.method == 'POST':
respiration_score_form = forms.RespirationScoreForm(request.POST)
if respiration_score_form.is_valid():
respiration_score = respiration_score_form.save(commit=False)
respiration_score.recorded = timezone.now()
respiration_score.user = request.user
respiration_score.save()
return redirect('mews-continue')
else:
respiration_score_form = forms.RespirationScoreForm()
context = {
'respiration_score_form': respiration_score_form,
}
return render(request, 'medwings/mews-init.html', context)
@login_required
def mews_continue(request):
return render(request, 'medwings/mews-continue.html')
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def mews_status(request): def mews_status(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -60,6 +92,7 @@ def mews_status(request):
body_temp_record=body_temp_record, body_temp_record=body_temp_record,
heart_rate_record=heart_rate_record, heart_rate_record=heart_rate_record,
respiration_score_record=respiration_score_record, respiration_score_record=respiration_score_record,
spo2_level_record=spo2_level_record
) )
mews_record.save() mews_record.save()
data['mews_value'] = mews_record.value_n data['mews_value'] = mews_record.value_n

View File

@ -2,6 +2,8 @@ asgiref==3.7.2
certifi==2023.7.22 certifi==2023.7.22
charset-normalizer==3.2.0 charset-normalizer==3.2.0
Django==4.2.3 Django==4.2.3
django-widget-tweaks==1.4.12
djangorestframework==3.14.0
idna==3.4 idna==3.4
psycopg==3.1.9 psycopg==3.1.9
psycopg-binary==3.1.9 psycopg-binary==3.1.9

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-29 18:35 # Generated by Django 4.2.3 on 2023-07-30 21:15
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@ -47,7 +47,22 @@ input[type="submit"] {
@apply hover:drop-shadow-xl; @apply hover:drop-shadow-xl;
} }
a.btn { input.error {
@apply border border-failure;
}
fieldset {
@apply border border-accent p-4;
}
legend {
@apply text-sm text-accent-600 px-2;
}
a {
@apply underline text-primary-300;
}
.btn {
text-align: center; text-align: center;
@apply rounded rounded-lg drop-shadow-md px-4 py-2; @apply rounded rounded-lg drop-shadow-md px-4 py-2;
@apply bg-accent-600; @apply bg-accent-600;
@ -55,7 +70,7 @@ a.btn {
@apply hover:drop-shadow-xl; @apply hover:drop-shadow-xl;
} }
a.btn-outline { .btn-outline {
text-align: center; text-align: center;
@apply rounded rounded-lg drop-shadow-md px-4 py-2; @apply rounded rounded-lg drop-shadow-md px-4 py-2;
@apply text-accent-600 bg-accent-600/10; @apply text-accent-600 bg-accent-600/10;
@ -93,10 +108,68 @@ div.status-message {
} }
div.status-message.error { div.status-message.error {
@apply bg-failure/50; @apply font-semibold bg-failure/50 border border-failure;
} }
div.call-to-action-box { div.call-to-action-box {
@apply bg-gradient-to-r from-secondary-300/75 to-secondary-500/75; @apply bg-gradient-to-r from-secondary-300/75 to-secondary-500/75;
@apply rounded-md py-4 px-6; @apply rounded-md py-4 px-6;
} }
.loader--loading {
animation: loading 2s infinite linear;
}
.loader--finished {
animation: finished 1.5s ease-in both;
}
.loader--value {
animation: loaded 1s ease-out both;
}
@keyframes loading {
0% {
transform: scaleX(0%) translateX(-100%);
transform-origin: left;
}
50% {
transform: scaleX(50%);
transform-origin: center;
}
100% {
transform: scaleX(0%) translateX(100%);
transform-origin: right;
}
}
@keyframes finished {
0% {
transform: scaleX(0%);
transform-origin: center;
}
50% {
transform: scaleX(100%);
transform-origin: center;
opacity: 1;
}
100% {
transform: scaleX(100%) translateX(1000%);
transform-origin: right;
opacity: 0;
}
}
@keyframes loaded {
0% {
transform: translateX(1000%);
opacity: 0;
}
100% {
transform: translateX(0%);
opacity: 1;
}
}

View File

@ -1,10 +1,10 @@
const navbarToggleButton = document.querySelector('#navbarToggleButton'); const navbarToggleButton = document.querySelector('#navbarToggleButton');
button.addEventListener('click', () => { //button.addEventListener('click', () => {
if (button.textContent === 'Click me') { //if (button.textContent === 'Click me') {
button.textContent = 'Clicked'; //button.textContent = 'Clicked';
} else { //} else {
button.textContent = 'Click me'; //button.textContent = 'Click me';
} //}
}); //});