merge experimental #1
@ -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>
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
{% 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 class="max-w-64" type="submit" value="Log In">
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
|
@ -29,6 +29,7 @@ ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'widget_tweaks',
|
||||
'core',
|
||||
'authentication',
|
||||
'medwings',
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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)),
|
||||
],
|
||||
),
|
||||
|
@ -119,3 +119,5 @@ class MewsRecord(models.Model):
|
||||
mews_value += 0
|
||||
else:
|
||||
mews_value += 2
|
||||
|
||||
return mews_value
|
||||
|
191
app/medwings/templates/medwings/mews-continue.html
Normal file
191
app/medwings/templates/medwings/mews-continue.html
Normal 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 %}
|
43
app/medwings/templates/medwings/mews-init.html
Normal file
43
app/medwings/templates/medwings/mews-init.html
Normal 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 %}
|
@ -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"),
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
//}
|
||||
//});
|
||||
|
Loading…
x
Reference in New Issue
Block a user