Skip to content

Commit bb9290e

Browse files
committed
Merge branch 'master' into develop
2 parents f8241bf + 7808051 commit bb9290e

File tree

15 files changed

+360
-62
lines changed

15 files changed

+360
-62
lines changed

README.md

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ Being Open Source, the application can be implemented within an organisation usi
1212

1313
Being modular, the application can be connected to an existing openEHR repository or operated with a stand-alone repository.
1414

15-
- [Screening question screenshot](https://github.com/AppertaFoundation/COVID-19-screening-interface/blob/master/frontend/screenshots/screening-questions.png)
16-
- [Screening date screenshot](https://github.com/AppertaFoundation/COVID-19-screening-interface/blob/master/frontend/screenshots/screening-questions-date.png)
17-
1815
## Project aims
1916

2017
The aims of this project are:
@@ -164,35 +161,37 @@ You can engage and contribute in the following ways:
164161
1. Clone the repo's, make changes and create a Pull Request (PR)
165162
2. Raise an issue on Github
166163
3. Join us on Slack, contact info@apperta.org for details
167-
4. Share with others
168-
5. add to the roadmap
164+
4. Share details of this project with others
165+
5. Add to the roadmap
169166

170167
## Team
171168

172-
Many people are involved in this project directly and indirectly and it builds on significant efforts of the industry and community. Some of the people in our team are:
169+
Many people are involved in this project directly and indirectly and it builds on significant efforts of the industry and community. Some of the people in our direct team are:
173170

174171
### openEHR clinical modelling
175172

176-
- Ian McNicoll (FreshEHR)
177-
- Alan Fish (Apperta)
173+
- Ian McNicoll ([openEHR](https://www.openehr.org/),[FreshEHR](https://freshehr.com/))
174+
- Alan Fish ([Apperta](https://apperta.org/))
178175

179176
### Clinical design
180177

181178
- Dr Alexander Davey
182179

183180
### System development
184181

185-
- JJ - Architecture (OpusVL)
186-
- James Curtis - Developer (OpusVL)
187-
- Adrianna - Front-end engineer (OpusVL)
188-
- Darren Wilson - User Experience (UX Centric)
189-
- Paul - Infrastructure engineer (OpusVL)
182+
- James C - Developer ([OpusVL](https://opusvl.com)
183+
- Adrianna - Front-end engineer ([OpusVL](https://opusvl.com)
184+
- Darren Wilson - User Experience ([UX Centric](https://www.uxcentric.co.uk/))
185+
- Paul B - Infrastructure engineer ([OpusVL](https://opusvl.com)
186+
- Nick - Developer ([OpusVL](https://opusvl.com)
187+
- Paul W - Developer ([OpusVL](https://opusvl.com)
188+
- Matt S - Developer (RBC/community)
190189

191190
### Project management
192191

193-
- David Jobling (Apperta)
194-
- Stuart Mackintosh (OpusVL)
195-
192+
- David Jobling - Project co-ordinator / analyst ([Apperta](https://apperta.org/))
193+
- JJ - Architect / project director ([OpusVL](https://opusvl.com))
194+
- Stuart Mackintosh ([OpusVL](https://opusvl.com)
196195

197196
## Contact
198197

c19-backend/C19/settings.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# SECURITY WARNING: don't run with debug turned on in production!
2626
DEBUG = True
2727

28-
ALLOWED_HOSTS = []
28+
ALLOWED_HOSTS = ['*']
2929

3030

3131
# Application definition
@@ -37,6 +37,8 @@
3737
'django.contrib.sessions',
3838
'django.contrib.messages',
3939
'django.contrib.staticfiles',
40+
'mozilla_django_oidc',
41+
'corsheaders',
4042
'rest_framework',
4143
'api',
4244
]
@@ -49,14 +51,15 @@
4951
'django.contrib.auth.middleware.AuthenticationMiddleware',
5052
'django.contrib.messages.middleware.MessageMiddleware',
5153
'django.middleware.clickjacking.XFrameOptionsMiddleware',
54+
'corsheaders.middleware.CorsMiddleware',
5255
]
5356

5457
ROOT_URLCONF = 'C19.urls'
5558

5659
TEMPLATES = [
5760
{
5861
'BACKEND': 'django.template.backends.django.DjangoTemplates',
59-
'DIRS': [],
62+
'DIRS': [os.path.join(BASE_DIR, 'templates')],
6063
'APP_DIRS': True,
6164
'OPTIONS': {
6265
'context_processors': [
@@ -107,12 +110,9 @@
107110

108111
REST_FRAMEWORK = {
109112
'DEFAULT_PERMISSION_CLASSES': [
110-
# 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
111-
# 'rest_framework.permissions.DjangoModelPermissions',
112-
'rest_framework.permissions.IsAuthenticated',
113+
'rest_framework.permissions.AllowAny', # until we have OAuth
113114
],
114115
'DEFAULT_AUTHENTICATION_CLASSES': [
115-
'rest_framework_simplejwt.authentication.JWTAuthentication',
116116
],
117117
}
118118

@@ -136,5 +136,25 @@
136136
STATIC_URL = '/static/'
137137

138138
EHRBASE_CONNECTION_PARAMS = dict(
139-
base_url=os.environ['C19_API_EHRBASE_URL'],
139+
base_url=os.environ['C19_BACKEND_EHRBASE_URL'],
140140
)
141+
142+
# mozilla-django-oidc
143+
144+
AUTHENTICATION_BACKENDS = (
145+
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
146+
)
147+
148+
OIDC_RP_CLIENT_ID = os.environ['OIDC_RP_CLIENT_ID']
149+
OIDC_RP_CLIENT_SECRET = os.environ['OIDC_RP_CLIENT_SECRET']
150+
OIDC_RP_SIGN_ALGO = os.environ['OIDC_RP_SIGN_ALGO']
151+
OIDC_RP_IDP_SIGN_KEY = os.environ['OIDC_RP_IDP_SIGN_KEY']
152+
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ['OIDC_OP_AUTHORIZATION_ENDPOINT']
153+
OIDC_OP_TOKEN_ENDPOINT = os.environ['OIDC_OP_TOKEN_ENDPOINT']
154+
OIDC_OP_USER_ENDPOINT = os.environ['OIDC_OP_USER_ENDPOINT']
155+
156+
LOGOUT_REDIRECT_URL = '/'
157+
LOGIN_REDIRECT_URL = '/'
158+
159+
CORS_ORIGIN_WHITELIST = tuple(
160+
os.environ['C19_BACKEND_CORS_ORIGIN_WHITELIST'].split('|'))

c19-backend/C19/urls.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
from django.contrib import admin
1717
from django.urls import path, include
1818

19+
from .views import HomePageView
20+
21+
1922
urlpatterns = [
23+
path('oidc/', include('mozilla_django_oidc.urls')),
2024
path('admin/', admin.site.urls),
21-
path('api/', include('api.urls'))
25+
path('api/', include('api.urls')),
26+
path('', HomePageView.as_view(), name='home')
27+
2228
]

c19-backend/C19/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.views.generic.base import TemplateView
2+
3+
4+
class HomePageView(TemplateView):
5+
6+
template_name = "home.html"

c19-backend/api/admin.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,22 @@
22
from django.contrib import admin
33

44
# Register your models here.
5-
from api.models import C19APIUserProfile
5+
from api.models import C19APIPatientProfile
66

7-
class C19APIUserProfileAdminForm(forms.ModelForm):
7+
class C19APIPatientProfileAdminForm(forms.ModelForm):
88
class Meta:
9-
model = C19APIUserProfile
9+
model = C19APIPatientProfile
1010
fields = (
1111
'user',
12-
'clinical_author_name',
13-
'clinical_author_id',
12+
'patient_nhs_number',
1413
)
1514
widgets = {
16-
'clinical_author_name': forms.TextInput(),
17-
'clinical_author_id': forms.TextInput(),
15+
'patient_nhs_number': forms.TextInput(),
1816
}
1917

2018

21-
class C19APIUserProfileAdmin(admin.ModelAdmin):
22-
form = C19APIUserProfileAdminForm
23-
list_display = ('user', 'clinical_author_name', 'clinical_author_id')
19+
class C19APIPatientProfileAdmin(admin.ModelAdmin):
20+
form = C19APIPatientProfileAdminForm
21+
list_display = ('user', 'patient_nhs_number')
2422

25-
admin.site.register(C19APIUserProfile, C19APIUserProfileAdmin)
23+
admin.site.register(C19APIPatientProfile, C19APIPatientProfileAdmin)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 3.0.4 on 2020-03-17 14:22
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('api', '0001_initial'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='C19APIPatientProfile',
18+
fields=[
19+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('patient_nhs_number', models.TextField()),
21+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='c19_api_user_profile', to=settings.AUTH_USER_MODEL)),
22+
],
23+
options={
24+
'verbose_name': 'Covid-19 API User Profile',
25+
'verbose_name_plural': 'Covid-19 API User Profiles',
26+
},
27+
),
28+
migrations.DeleteModel(
29+
name='C19APIUserProfile',
30+
),
31+
]

c19-backend/api/models.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from django.conf import settings
22
from django.db import models
33

4-
class C19APIUserProfile(models.Model):
4+
# TODO probably won't be needed once OAuth is set up
5+
# remember to generate migration if you do remove it
6+
class C19APIPatientProfile(models.Model):
57
user = models.OneToOneField(
68
settings.AUTH_USER_MODEL,
7-
related_name='c19_api_user_profile',
9+
related_name='c19_api_patient_profile',
810
on_delete=models.CASCADE)
9-
clinical_author_name = models.TextField()
10-
clinical_author_id = models.TextField()
11+
# Need for this might disappear once using NHS login
12+
patient_nhs_number = models.TextField()
1113

1214
def __str__(self):
1315
return str(self.user)
1416

1517
class Meta:
16-
verbose_name = 'Covid-19 API User Profile'
17-
verbose_name_plural = 'Covid-19 API User Profiles'
18+
verbose_name = 'Covid-19 API Patient Profile'
19+
verbose_name_plural = 'Covid-19 API Patient Profiles'

c19-backend/api/urls.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
from rest_framework import routers
44
from api import views
55
from rest_framework.urlpatterns import format_suffix_patterns
6-
from rest_framework_simplejwt.views import (
7-
TokenObtainPairView,
8-
TokenRefreshView,
9-
)
106

117

128
router = routers.DefaultRouter()
@@ -16,12 +12,4 @@
1612
urlpatterns = [
1713
path('', include(router.urls)),
1814
path('0.1/covid-screenings/', views.CovidScreenListView.as_view()),
19-
path(
20-
'0.1/auth/token/',
21-
TokenObtainPairView.as_view(),
22-
name='token_obtain_pair'),
23-
path(
24-
'0.1/auth/token/refresh/',
25-
TokenRefreshView.as_view(),
26-
name='token_refresh'),
2715
]

c19-backend/api/views.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@
33
from rest_framework.response import Response
44
from rest_framework import status
55

6+
from ehrbase_connector.connector import OpenEHRAPI
67
from . import ehrbase
78

89
class CovidScreenListView(APIView):
9-
1010
def post(self, request, format=None):
11-
# serializer = Covid19Serializer(data=request.data)
12-
# if serializer.is_valid():
13-
# ehrbase.CONNECTION.post(data=serializer.data)
14-
return Response(data={'fakeresponse': 'foo'})
11+
screening_data = request.data
12+
ehr_api = OpenEHRAPI(connection=ehrbase.CONNECTION)
13+
ehr_id = ehr_api.ehr_id_for_nhs_number(
14+
nhs_number=screening_data['nhs_number'])
15+
return Response(
16+
data={
17+
# TODO we probably won't send the nhs number back,
18+
# this is just for stubbing to check the code branches
19+
'nhs_number': screening_data['nhs_number'],
20+
'ehr_id': ehr_id,
21+
'_note':
22+
'Just a fake return value for stubbing purposes for now,'
23+
' and will probably change completely',
24+
},
25+
)

c19-backend/ehrbase_connector/connector.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import os
33
import requests
4+
import requests.exceptions
45

56
import attr
67

@@ -13,5 +14,70 @@ def connect(*, base_url) -> 'OpenEHRConnector':
1314
class OpenEHRConnector(object):
1415
base_url = attr.ib()
1516

16-
def get(self, path):
17-
return requests.get(self.base_url + path)
17+
def _url(self, suffix):
18+
return self.base_url + suffix
19+
20+
def get(self, path, params=None, **kwargs):
21+
# TODO also pass auth=(user, pass) once basic auth implemented
22+
return requests.get(self._url(path), params=params, **kwargs)
23+
24+
def post(self, path, data=None, json=None, **kwargs):
25+
# TODO also pass auth=(user, pass) once basic auth implemented
26+
return requests.post(self._url(path), data=data, json=json, **kwargs)
27+
28+
29+
@attr.s(frozen=True)
30+
class OpenEHRAPI(object):
31+
connection = attr.ib()
32+
33+
def ehr_id_for_nhs_number(self, *, nhs_number: str):
34+
def ehr_already_existed(status_code):
35+
return status_code == 409 # Conflict
36+
nhs_number_namespace = 'uk.nhs.number'
37+
creation_response = self.connection.post(
38+
'/ehrbase/rest/openehr/v1/ehr',
39+
json={
40+
"_type": "EHR_STATUS",
41+
"subject": {
42+
"external_ref": {
43+
"id": {
44+
"_type": "GENERIC_ID",
45+
"value": nhs_number,
46+
"scheme": "uk.nhs.nhs_number"
47+
},
48+
"namespace": nhs_number_namespace,
49+
"type": "PERSON"
50+
}
51+
},
52+
"is_modifiable": "true",
53+
"is_queryable": "true",
54+
},
55+
)
56+
if creation_response.status_code == requests.codes.ok \
57+
or ehr_already_existed(status_code=creation_response.status_code)\
58+
:
59+
# For now even if the POST was successful we have to GET because
60+
# EHRBase sends empty body with status 204 instead of 201 with some
61+
# JSON
62+
fetch_response = self.connection.get(
63+
'/ehrbase/rest/openehr/v1/ehr',
64+
params={
65+
'subject_id': nhs_number,
66+
'subject_namespace': nhs_number_namespace,
67+
},
68+
)
69+
if fetch_response.status_code == requests.codes.ok:
70+
return fetch_response.json()['ehr_id']['value']
71+
else:
72+
raise APIException(
73+
"HTTP status {status} during GET /ehrbase/rest/openehr/v1/ehr"
74+
.format(status=fetch_response.status_code)
75+
)
76+
else:
77+
raise APIException(
78+
"HTTP status {status} during POST /ehrbase/rest/openehr/v1/ehr"
79+
)
80+
81+
82+
class APIException(Exception):
83+
pass

0 commit comments

Comments
 (0)