merge experimental #1

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

View File

@ -79,6 +79,7 @@ def register_continue(request):
user_password = request.POST.get('password1') user_password = request.POST.get('password1')
gotify_user_info = gotify.api.create_user(user.username, user_password) gotify_user_info = gotify.api.create_user(user.username, user_password)
gotify_app_info = gotify.api.create_application(user.username, user_password) gotify_app_info = gotify.api.create_application(user.username, user_password)
gotify.api.upload_application_picture(user.username, user_password, gotify_app_info['id'])
gotify_user = gotify.models.GotifyUser( gotify_user = gotify.models.GotifyUser(
user=user, user=user,
id=gotify_user_info['id'] id=gotify_user_info['id']

View File

@ -56,3 +56,20 @@ def create_application(username: str, password: str) -> dict:
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def upload_application_picture(username: str, password: str, app_id: int):
with open('/app/static/medwings/images/logo/medwings-logo.png', 'rb') as image_file:
response = requests.post(
url=f"http://{settings.GOTIFY_CONFIG['HOST']}/application/{app_id}/image",
auth=HTTPBasicAuth(
username,
password,
),
files={
'file': image_file
}
)
if response is not None:
response.raise_for_status()

View File

View File

@ -0,0 +1,24 @@
from django.urls import reverse
from django.core.management.base import BaseCommand
from gotify import models
class Command(BaseCommand):
help = 'Notifies all users to take a vitals measurement now.'
def handle(self, *args, **kwargs):
applications = models.GotifyApplication.objects.all()
for application in applications:
message_text = f"Hello {application.user.user.first_name}. Please take your next vitals measurement now."
message_title = "Medwings Measurement Prompt"
url = reverse('mews-init')
message = models.GotifyMessage(
message=message_text,
title=message_title,
url=url
)
application.send_message(message)

View File

@ -1,6 +1,10 @@
from django.db import models from enum import Enum
import json
from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings
import requests
class GotifyUser(models.Model): class GotifyUser(models.Model):
@ -8,7 +12,68 @@ class GotifyUser(models.Model):
id = models.PositiveIntegerField(verbose_name="Gotify User ID") id = models.PositiveIntegerField(verbose_name="Gotify User ID")
class GotifyMessageType(Enum):
plain = "text/plain"
markdown = "text/markdown"
class GotifyMessage():
type: GotifyMessageType
message: str
title: str | None
priority: int
url: str | None
def __init__(self, message: str, title: str | None = None, priority: int = 5, url: str | None = None, type: str = 'text/plain'):
self.message = message
self.title = title
if not 0 <= priority <= 10:
raise ValueError(f"Priority must be 0 to 10.")
self.priority = priority
self.url = url
self.type = GotifyMessageType(type)
def as_dict(self) -> dict:
obj = {
"message": self.message,
"priority": self.priority,
"extras": {
"client::display": {
"contentType": self.type.value
}
}
}
if self.title:
obj["title"] = self.title
if self.url:
obj["extras"]["client::notification"] = {
"click": {
"url": self.url
}
}
return obj
class GotifyApplication(models.Model): class GotifyApplication(models.Model):
user = models.OneToOneField(GotifyUser, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(GotifyUser, on_delete=models.CASCADE, primary_key=True)
id = models.PositiveIntegerField(verbose_name="Gotify Application ID") id = models.PositiveIntegerField(verbose_name="Gotify Application ID")
token = models.CharField(max_length=256, verbose_name="Gotify Application Token") token = models.CharField(max_length=256, verbose_name="Gotify Application Token")
def send_message(self, message: GotifyMessage):
endpoint_url = f"http://{settings.GOTIFY_CONFIG['HOST']}/message"
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.post(
url=endpoint_url,
headers=headers,
data=json.dumps(message.as_dict())
)
if response is not None:
response.raise_for_status()

View File

@ -8,7 +8,6 @@
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl"> <div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
<h1>Welcome to Medwings</h1> <h1>Welcome to Medwings</h1>
<p1>Your personal health guardian</p1> <p1>Your personal health guardian</p1>
{% comment %}<img src="{% static 'medwings/images/devices/withings-thermo.webp' %}" alt="A withings thermometer.">{% endcomment %}
<p> <p>
We understand that after receiving medical care, you may still have concerns about your health, particularly if you're at We understand that after receiving medical care, you may still have concerns about your health, particularly if you're at
risk of sudden health changes. risk of sudden health changes.

View File

@ -8,6 +8,14 @@
{% block content %} {% block content %}
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl"> <div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
<h1>Record your health status</h1> <h1>Record your health status</h1>
<div class="flex flex-col justify-center items-center gap-2">
<div class="flex justify-center items-center gap-4">
<img class="max-h-24 max-w-24 drop-shadow-lg saturate-50" src="{% static 'medwings/images/devices/withings-bpm-core.webp' %}" alt="A Withings BPM core.">
<img class="max-h-24 max-w-24 drop-shadow-lg saturate-50" src="{% static 'medwings/images/devices/withings-thermo.webp' %}" alt="A Withings Thermo.">
<img class="max-h-24 max-w-24 drop-shadow-lg saturate-50" src="{% static 'medwings/images/devices/withings-scanwatch.webp' %}" alt="A Withings Scanwatch.">
</div>
<p class="text-center sm:text-start font-semibold text-lg">Before you begin, please have your Withings devices ready for taking measurements.</p>
</div>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<fieldset class="flex flex-col gap-4 items-center"> <fieldset class="flex flex-col gap-4 items-center">

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
version="1.1"
id="Capa_1"
width="800.00897"
height="800.00897"
viewBox="0 0 45.968516 45.968516"
xml:space="preserve"
sodipodi:docname="medwings-logo.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
inkscape:export-filename="medwings-logo.png"
inkscape:export-xdpi="30.719656"
inkscape:export-ydpi="30.719656"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4"><linearGradient
id="linearGradient7"
inkscape:collect="always"><stop
style="stop-color:#ec3636;stop-opacity:1;"
offset="0"
id="stop7" /><stop
style="stop-color:#e2df46;stop-opacity:1;"
offset="1"
id="stop8" /></linearGradient>&#10; &#10;<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7"
id="linearGradient8"
x1="-0.88695079"
y1="35.375"
x2="46.902077"
y2="9.6768303"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7"
id="linearGradient9"
gradientUnits="userSpaceOnUse"
x1="-0.88695079"
y1="35.375"
x2="46.902077"
y2="9.6768303" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7"
id="linearGradient10"
gradientUnits="userSpaceOnUse"
x1="-0.88695079"
y1="35.375"
x2="46.902077"
y2="9.6768303" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7"
id="linearGradient11"
gradientUnits="userSpaceOnUse"
x1="-0.88695079"
y1="35.375"
x2="46.902077"
y2="9.6768303" /></defs><sodipodi:namedview
id="namedview4"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.0275"
inkscape:cx="400"
inkscape:cy="371.77616"
inkscape:window-width="1900"
inkscape:window-height="1004"
inkscape:window-x="10"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />&#10;<g
id="g3"
style="fill:url(#linearGradient8);fill-opacity:1"
transform="translate(1.3144378e-4,-3.0572798e-4)">&#10; <path
d="m 44.887,10.992 c -1.108,-0.732 -2.603,-0.425 -3.33,0.683 -1.736,2.631 -4.229,3.869 -6.704,4.869 h -0.002 c 2.439,3 2.14,8.199 -0.928,11.267 -0.052,0.051 -1.104,1.101 -2.487,2.477 1.027,-0.073 2.096,-0.217 3.179,-0.461 3.148,-0.713 6.408,-2.269 9.085,-5.335 0.872,-1 0.77,-2.521 -0.231,-3.394 -0.013,-0.009 -0.023,-0.015 -0.036,-0.024 0.561,-0.555 1.097,-1.166 1.603,-1.842 0.796,-1.063 0.58,-2.57 -0.482,-3.366 -0.027,-0.021 -0.06,-0.033 -0.087,-0.052 0.386,-0.464 0.756,-0.958 1.105,-1.489 0.73,-1.11 0.423,-2.602 -0.685,-3.333 z"
id="path1"
style="fill:url(#linearGradient9);fill-opacity:1;stroke:none" />&#10; <path
d="m 9.478,21.624 c 0,-1.85 0.578,-3.606 1.636,-5.079 C 8.638,15.759 6.146,14.307 4.411,11.676 3.682,10.568 2.189,10.26 1.081,10.992 c -1.108,0.731 -1.415,2.223 -0.684,3.332 0.351,0.531 0.721,1.025 1.105,1.489 -0.027,0.019 -0.059,0.031 -0.086,0.052 -1.063,0.796 -1.279,2.303 -0.483,3.366 0.506,0.676 1.042,1.287 1.603,1.842 C 2.523,21.082 2.512,21.088 2.5,21.097 1.499,21.97 1.396,23.49 2.268,24.49 c 2.677,3.067 5.937,4.623 9.085,5.336 1.083,0.244 2.151,0.388 3.18,0.461 -1.386,-1.38 -2.442,-2.432 -2.493,-2.482 -1.653,-1.651 -2.562,-3.846 -2.562,-6.181 z"
id="path2"
style="fill:url(#linearGradient10);fill-opacity:1;stroke:none" />&#10; <path
d="m 32.483,16.857 c -1.316,-1.316 -3.042,-1.974 -4.769,-1.974 -1.717,0 -3.435,0.652 -4.748,1.956 -1.314,-1.304 -3.031,-1.956 -4.749,-1.956 -1.726,0 -3.45,0.658 -4.767,1.974 -2.633,2.632 -2.633,6.902 0,9.535 0.128,0.128 6.376,6.346 8.677,8.636 0.231,0.231 0.535,0.347 0.839,0.347 0.304,0 0.607,-0.115 0.839,-0.347 2.301,-2.29 8.549,-8.507 8.677,-8.636 2.633,-2.634 2.633,-6.903 10e-4,-9.535 z"
id="path3"
style="fill:url(#linearGradient11);fill-opacity:1;stroke:none" />&#10; </g>&#10;</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -2,6 +2,9 @@
FROM python:alpine FROM python:alpine
# Install cron daemon and supervisord
RUN apk add --no-cache dcron supervisor
# Create non-root user # Create non-root user
ARG CUSTOM_UID ARG CUSTOM_UID
ARG CUSTOM_GID ARG CUSTOM_GID
@ -12,14 +15,19 @@ RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \
mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app
ENV PATH "$PATH:/home/${CUSTOM_GROUPNAME}/.local/bin" ENV PATH "$PATH:/home/${CUSTOM_GROUPNAME}/.local/bin"
# Add supervisord conf
COPY development.supervisord.conf /etc/supervisord.conf
# Add cron job
COPY --chmod=600 django.crontab /etc/crontabs/django
# Copy source files # Copy source files
WORKDIR /app WORKDIR /app
COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} app/ /app/ COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} app/ /app/
# Install dependencies # Install dependencies
USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000}
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
# Run ASGI server # Run supervisord
EXPOSE 8000/tcp EXPOSE 8000/tcp
ENTRYPOINT ["python", "manage.py", "runserver", "0.0.0.0:8000"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -23,7 +23,7 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- medwings-caddy - medwings-caddy
- medwings-postgres - ${PG_HOST}
build: build:
context: . context: .
dockerfile: ./development.django.Dockerfile dockerfile: ./development.django.Dockerfile
@ -70,8 +70,6 @@ services:
image: gotify/server image: gotify/server
container_name: medwings-gotify container_name: medwings-gotify
restart: unless-stopped restart: unless-stopped
depends_on:
- medwings-postgres
ports: ports:
- "8001:80" - "8001:80"
volumes: volumes:
@ -89,7 +87,7 @@ services:
container_name: medwings-pgweb container_name: medwings-pgweb
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- medwings-postgres - ${PG_HOST}
ports: ports:
- "8002:8081" - "8002:8081"
environment: environment:

View File

@ -0,0 +1,17 @@
[supervisord]
nodaemon=true
user=root
[program:django]
command=sh -c 'python manage.py runserver 0.0.0.0:8000'
directory=/app
user=django
autostart=true
autorestart=true
redirect_stderr=true
[program:crond]
command=crond -f
autostart=true
autorestart=true
redirect_stderr=true

1
django.crontab Normal file
View File

@ -0,0 +1 @@
0 10,13,16,19,22 * * * python /app/manage.py notify_all