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' %}
{% load static %}
{% load widget_tweaks %}
{% block title %}
Medwings | Log In
{% endblock title %}
{% block content %}
<div class="flex flex-col justify-center items-center gap-2 py-4">
{% if form.errors %}
<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 %}
<h1>Log In</h1>
<form method="post" action="{% url 'login' %}">
{% 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>
<div class="grid grid-cols-4 justify-center items-center gap-2">
<div class="col-span-1">{{ form.username.label_tag }}</div>
<div class="col-span-3">{{ form.username }}</div>
<div class="col-span-1">{{ form.password.label_tag }}</div>
<div class="col-span-3">{{ form.password }}</div>
<input class="col-span-4" type="submit" value="Log In">
{% if form.non_field_errors %}
<div class="flex flex-col gap-2 status-message error">
{% for error in form.non_field_errors %}
<p class="error">{{ error }}</p>
{% 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>
<input type="hidden" name="next" value="{{ next }}">
<input class="max-w-64" type="submit" value="Log In">
</fieldset>
</form>

View File

@ -29,6 +29,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'widget_tweaks',
'core',
'authentication',
'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.db import migrations, models

View File

@ -1,6 +1,6 @@
from django import forms
from medwings.models import Profile
from medwings.models import Profile, RespirationScoreRecord
class ProfileForm(forms.ModelForm):
@ -10,3 +10,9 @@ class ProfileForm(forms.ModelForm):
widgets = {
'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.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')),
('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')),
('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)),
],
),

View File

@ -119,3 +119,5 @@ class MewsRecord(models.Model):
mews_value += 0
else:
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 = [
path("", views.index, name="home"),
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"),
]

View File

@ -1,12 +1,13 @@
from datetime import timedelta
from django.shortcuts import render
from django.shortcuts import redirect, render
from django.http import HttpResponse, JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.utils import timezone
from . import models
from . import forms
@require_http_methods(["GET"])
@ -14,10 +15,41 @@ def index(request):
return render(request, 'medwings/index.html')
@login_required
@require_http_methods(["GET"])
def dashboard(request):
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"])
def mews_status(request):
if not request.user.is_authenticated:
@ -60,6 +92,7 @@ def mews_status(request):
body_temp_record=body_temp_record,
heart_rate_record=heart_rate_record,
respiration_score_record=respiration_score_record,
spo2_level_record=spo2_level_record
)
mews_record.save()
data['mews_value'] = mews_record.value_n

View File

@ -2,6 +2,8 @@ asgiref==3.7.2
certifi==2023.7.22
charset-normalizer==3.2.0
Django==4.2.3
django-widget-tweaks==1.4.12
djangorestframework==3.14.0
idna==3.4
psycopg==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.db import migrations, models

View File

@ -47,7 +47,22 @@ input[type="submit"] {
@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;
@apply rounded rounded-lg drop-shadow-md px-4 py-2;
@apply bg-accent-600;
@ -55,7 +70,7 @@ a.btn {
@apply hover:drop-shadow-xl;
}
a.btn-outline {
.btn-outline {
text-align: center;
@apply rounded rounded-lg drop-shadow-md px-4 py-2;
@apply text-accent-600 bg-accent-600/10;
@ -93,10 +108,68 @@ div.status-message {
}
div.status-message.error {
@apply bg-failure/50;
@apply font-semibold bg-failure/50 border border-failure;
}
div.call-to-action-box {
@apply bg-gradient-to-r from-secondary-300/75 to-secondary-500/75;
@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');
button.addEventListener('click', () => {
if (button.textContent === 'Click me') {
button.textContent = 'Clicked';
} else {
button.textContent = 'Click me';
}
});
//button.addEventListener('click', () => {
//if (button.textContent === 'Click me') {
//button.textContent = 'Clicked';
//} else {
//button.textContent = 'Click me';
//}
//});