apraxostux

Author: Dr. med Claudia Neumann
© 2021 Dr. Claudia Neumann

Update: 15.5.2021

Einleitung

Die Verordnungssoftware-Schnittstelle (VOS) der KBV, die von jedem Praxisverwaltungsprogramm (PVS) zertifiziert werden muss, ist wieder mal ein Beispiel, wie aus einer guten Idee Müll werden kann.

Die VOS sollte eingerichtet werden, damit die Abhängigkeit des Arztes von einem Medikamenten-Datenbank-Anbieter, der von seinem Software-Haus bestimmt wird, aufzulösen. Damit sollte der Arzt die Medikamenten-Datenbanken austauschen können und damit vielleicht auch Geld sparen können.

Meine Idee war, dass das PVS eine Anfrage über die VOS an die Medikamenten-Datenbank stellt und damit die Rezeptschreibung aus dem PVS ermöglicht.

Das ist allerdings nicht der Fall. Das PVS muss den REST-Server für die Anfrage zur Verfügung stellen. Dann soll die Medikamenten-Datenbank-Software aufgerufen werden. Medikamente sollen in der Medikamenten-Datenbank-Software ausgewählt werden und dann das Rezept von dort ausgedruckt werden.

Da die Medikamenten-Datenbank-Software meines Wissens nur auf Windows läuft, heißt das, dass der Linux-Anwender die Medikamenten-Datenbank-Software entweder in einem Windows in virtualbox oder an einem anderen Rechner laufen lassen müsste. Abgesehen davon, dass apraxos in seiner IT-Sicherheits- Richtlinie möglichst alle Windows-Rechner aus dem Praxis-Netzwerk entfernt haben möchte, ist so ein Verordnungsablauf nicht praktikabel und damit für apraxos unbrauchbar.

Trotzdem beschreibe ich hier, wie der REST-Server für eine VOS eingerichtet werden muss, um den Zertifizierungsanforderungen der KBV zu entsprechen.

web2py auf Debian Buster installieren

Man lege als root einen neuen Benutzer www-data an, wechsele als root in das neuen /home/www-data-Verzeichnis und lade die aktuelle web2py-Software herunter:

cd /home/www-data
wget http://web2py.com/examples/static/web2py_src.zip
unzip web2py_src.zip
chown -R www-data:www-data /home/www-data/web2py

weiter als root:

cd /home/www-data/web2py
sudo -u www-data python -c "from gluon.main import save_password; save_password(raw_input('admin password: '), 443)"

admin passwort eingeben. Anlegen eines selbst-signierten Server-Zertificats für die TLS-Übertragung:

openssl genrsa 2048 > /home/www-data/web2py/server.key
chmod 400 /home/www-data/web2py/server.key
openssl req -new -x509 -nodes -sha1 -days 365 -key /home/www-data/web2py/server.key > /home/www-data/web2py/server.crt
openssl x509 -noout -fingerprint -text < /home/www-data/web2py/server.crt > /home/www-data/web2py/server.csr
chown -R www-data:www-data /home/www-data/web2py

Nun kann man web2py mit:

python3 web2py.py -c server.crt -k server.key -i 127.0.0.1 -p 8000

als root starten. Da root in Debian keine grafische Oberfläche darstellen kann, gibt es eine Fehlermeldung:

ERROR:web2py:cannot get Tk root window, GUI disabled
Traceback (most recent call last):
  File "/home/www-data/web2py/gluon/widget.py", line 792, in start
    root = tkinter.Tk()
  File "/usr/lib/python3.7/tkinter/__init__.py", line 2023, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
_tkinter.TclError: no display name and no $DISPLAY environment variable

Das kann man ignorieren oder man startet mit sudo. Es wird die Festlegung eines Passworts verlangt, mit dem man sich später über die Webseite anmelden kann.

Wenn man mit sudo startet, wird das web2py-Framework-Fenter angezeigt:

web2py1

und auch dort muss ein Passwort festgelegt werden. Danach läuft der web2py-Service.

Im Browser (Firefox) kann man nun https://127.0.0.1:8000 aufrufen * Sicherheitswarnung aktzeptieren * mit Passwort einloggen

Nun läuft web2py als Webservice auf dem Rechner mit der IP-Adresse 127.0.0.1 oder localhost.

web2py2
web2py3

REST-Server mit web2py einrichten

Über die Konfiguratons-Oberfläche unter https://127.0.0.1:8000 legt man ein neues Modul vos an. Dabei wird ein neues Unterverzeichnis /home/www-data/web2py/applications/vos angelegt.

web2py4

Die weiteren Konfigurationen können über die Konfigurations-Oberfläche im Browser oder auf der Kommandozeile als root ausgeführt werden:

/home/www-data/web2py/applications/vos/models/db.py:

auth.settings.reset_password_requires_verification = True

# -------------------------------------------------------------------------
# read more at http://dev.w3.org/html5/markup/meta.name.html
# -------------------------------------------------------------------------
response.meta.author = configuration.get('app.author')
response.meta.description = configuration.get('app.description')
response.meta.keywords = configuration.get('app.keywords')
response.meta.generator = configuration.get('app.generator')
response.show_toolbar = configuration.get('app.toolbar')

# -------------------------------------------------------------------------
# your http://google.com/analytics id
# -------------------------------------------------------------------------
response.google_analytics_id = configuration.get('google.analytics_id')

# -------------------------------------------------------------------------
# maybe use the scheduler
# -------------------------------------------------------------------------
if configuration.get('scheduler.enabled'):
    from gluon.scheduler import Scheduler
    scheduler = Scheduler(db, heartbeat=configuration.get('scheduler.heartbeat'))

# -------------------------------------------------------------------------
# Define your tables below (or better in another model file) for example
#
# >>> db.define_table('mytable', Field('myfield', 'string'))
#
# Fields can be 'string','text','password','integer','double','boolean'
#       'date','time','datetime','blob','upload', 'reference TABLENAME'
# There is an implicit 'id integer autoincrement' field
# Consult manual for more options, validators, etc.
#
# More API examples for controllers:
#
# >>> db.mytable.insert(myfield='value')
# >>> rows = db(db.mytable.myfield == 'value').select(db.mytable.ALL)
# >>> for row in rows: print row.id, row.myfield
# -------------------------------------------------------------------------

# -------------------------------------------------------------------------
# after defining tables, uncomment below to enable auditing
# -------------------------------------------------------------------------
# auth.enable_record_versioning(db)


db.define_table("entries", Field("entry", "text"))

Damit wird eine neue Datenbank entries mit sqlite mit dem Feld entry als Textfeld angelegt. In diese Datenbank werden die zu übertragenden Daten abgelegt.

/home/www-data/web2py/applications/vos/private/appconfig.ini:

; App configuration
[app]
name        = Welcome
author      = Dr. Claudia Neuman <neumann@apraxos.de>
description = Proof of Concept VOS
keywords    = web2py, python, framework
generator   = Web2py Web Framework
production  = false
toolbar     = false

; Host configuration
[host]
names = localhost:*, 127.0.0.1:*, *:*, *

; db configuration
[db]
uri       = sqlite://my.xml.storage.sqlite
migrate   = false
pool_size = 10000

; smtp address and credentials
[smtp]
server = smtp.gmail.com:587
sender = you@gmail.com
login  = username:password
tls    = true
ssl    = true

[scheduler]
enabled   = false
heartbeat = 1

[google]
analytics_id =

Konfigurationen für den Web-Service.

/home/www-data/web2py/applications/vos/controllers/default.py:

# -*- coding: utf-8 -*-
# -------------------------------------------------------------------------
# This is a sample controller
# this file is released under public domain and you can use without limitations
# -------------------------------------------------------------------------

# ---- example index page ----
def index():
    response.flash = T("Hello World")
    return dict(message=T('Welcome to web2py!'))

# ---- API (example) -----
@auth.requires_login()
def api_get_user_email():
    if not request.env.request_method == 'GET': raise HTTP(403)
    return response.json({'status':'success', 'email':auth.user.email})

# ---- Smart Grid (example) -----
@auth.requires_membership('admin') # can only be accessed by members of admin groupd
def grid():
    response.view = 'generic.html' # use a generic view
    tablename = request.args(0)
    if not tablename in db.tables: raise HTTP(403)
    grid = SQLFORM.smartgrid(db[tablename], args=[tablename], deletable=False, editable=False)
    return dict(grid=grid)

# ---- Embedded wiki (example) ----
def wiki():
    auth.wikimenu() # add the wiki to the menu
    return auth.wiki()

# ---- Action for login/register/etc (required for auth) -----
def user():
    """
    exposes:
    http://..../[app]/default/user/login
    http://..../[app]/default/user/logout
    http://..../[app]/default/user/register
    http://..../[app]/default/user/profile
    http://..../[app]/default/user/retrieve_password
    http://..../[app]/default/user/change_password
    http://..../[app]/default/user/bulk_register
    use @auth.requires_login()
        @auth.requires_membership('group name')
        @auth.requires_permission('read','table name',record_id)
    to decorate functions that need access control
    also notice there is http://..../[app]/appadmin/manage/auth to allow administrator to manage users
    """
    return dict(form=auth())

# ---- action to server uploaded static content (required) ---
@cache.action()
def download():
    """
    allows downloading of uploaded files
    http://..../[app]/default/download/[filename]
    """
    return response.download(request, db)


@request.restful()
def api():
    response.view = 'generic.xml'
    def GET(*args,**vars):
        patterns = 'auto'
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):

        return dict(db[table_name].validate_and_insert(**vars))
    def PUT(table_name,record_id,**vars):
        return dict(db(db[table_name]._id==record_id).update(**vars))
    def DELETE(table_name,record_id):
        return dict(db(db[table_name]._id==record_id).delete())
    return dict(GET=GET, POST=POST, PUT=PUT, DELETE=DELETE)

Vorsichtshalber nochmal

chown -R www-data.www-data /home/www-data/web2py

Es muss mindestens ein User angelegt werden. Über die Datenbankadministration

web2py5

legen Sie einen neuen User mit "Neuer Eintrag" bei db.auth_user an.

web2py7

Leider ist hier was bei der Übersetzung schief gegangen. Der folgende Screenshot sollte erklären, welche Felder ausgefüllt werden müssen. cognom (Benutzer) und contrysenya (Passwort) müssen in apraxos eingegeben werden, damit die Nachricht angenommen wird.

web2py8

Nun sollte der Web-Service neu gestartet werden.

Mit

curl -vvv --user <Benutzer>:<Passwort> -d 'entry=<Allergie>Nuss</Allergie>' -k https://127.0.0.1:8000/vos/default/api/entries

kann auf der Kommandozeile getestet werden, ob der REST-Service funktioniert.

curl -vvv --user neumann:########## -d 'entry=<Allergie>Nuss</Allergie>' -k https://127.0.0.1:8000/vos/default/api/entries
* Expire in 0 ms for 6 (transfer 0x564aec67dfb0)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x564aec67dfb0)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=DE; ST=Deutschland; L=Friesoythe; O=Dr. Claudia Neumann - EDV-Beratung; OU=Dr. Claudia Neumann; CN=Dr. Claudia Neumann; emailAddress=neumann@apraxos.de
*  start date: May  4 13:28:12 2021 GMT
*  expire date: May  4 13:28:12 2022 GMT
*  issuer: C=DE; ST=Deutschland; L=Friesoythe; O=Dr. Claudia Neumann - EDV-Beratung; OU=Dr. Claudia Neumann; CN=Dr. Claudia Neumann; emailAddress=neumann@apraxos.de
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
* Server auth using Basic with user 'neumann'
> POST /vos/default/api/entries HTTP/1.1
> Host: 127.0.0.1:8000
> Authorization: Basic bmV1bWFubjpDbFdlTmUxNQ==
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Length: 31
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 31 out of 31 bytes
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 200 OK
< X-Powered-By: web2py
< Content-Type: text/html; charset=utf-8
< Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< Expires: Sat, 15 May 2021 17:14:59 GMT
< Pragma: no-cache
< Set-Cookie:  session_id_vos=127.0.0.1-a50859fd-b562-4e60-a8ff-2d0076a86195; HttpOnly; Path=/; SameSite=Lax
< Date: Sat, 15 May 2021 17:14:59 GMT
< Server: Rocket 1.2.6 Python/3.7.3
< Content-Length: 88
< Connection: keep-alive
<
<?xml version="1.0" encoding="UTF-8"?><document><id>48</id><errors></errors></document>
* Connection #0 to host 127.0.0.1 left intact

Die Antwort des Servers hier ist:

<?xml version="1.0" encoding="UTF-8"?><document><id>48</id><errors></errors></document>

D.h. es wurde ein Dokument mit der ID 48 ohne Fehler angenommen.

In der Sqlite-Tabelle befindet sich jetzt ein Eintrag 48 mit <Allergie>Nuss</Allergie> im Feld entry.

web2py9

Das kleine XML-Schnipsel <Allergie>Nuss</Allergie> wurde gespeichert und steht zum Abruf bereit.

Da die Medikamenten-Datenbank entweder in einer virtuellen Maschine oder auf einem anderen Rechner laufen wird, muss die URL von 127.0.0.1 auf die aktuelle IP des REST-Servers umgestellt werden. Dann muss der REST-Server mit:

python3 web2py.py -c server.crt -k server.key -i <IP-Adresse des REST-Servers> -p 8000

gestartet werden.

Konfiguration in apraxos

Unter System / Schalter / Medikamente muss Verordnungssoftware auf 1 gesetzt werden. Dann kann unter System / Einstellungen / VOS-SST aufgerufen werden:

web2py10

Wenn apraxos so konfiguriert wurde, werden beim Aufruf des Kassenrezeptes die Daten des Patienten mit den Praxis-/Arztdaten an den REST-Server versandt. In der Medikamenten-Datenbank müssen diese Daten abgerufen werden. Theoretisch soll die Medikamenten-Datenbank aus apraxos heraus aufgerufen werden und deren Bildschirm in den Vordergrund geladen werden. Wie ich das machen soll, ist mir allerdings schleierhaft. Sollte die Medikamenten-Datenbank in einer virtuellen Maschine laufen, müsste diese von Hand in den Vordergrund geholt werden.

Rezepte nach dieser Methode verordnen zu wollen, ist vollig sinnfrei und nicht praktikabel. Diese Dokumentation dient lediglich der Zertifizierung.