Skip to content

Commit

Permalink
Initial docker setup
Browse files Browse the repository at this point in the history
Docker issues mostly sorted. Compose either doesn't transfer context or handle volumes properly under windows. works fine under OSX

Initial setup with mysql; migrations, etc

Inital models; dbsnapshot script

Adds bone-stock admin to site

adds time_spent_waiting property to the appointment object

oauth2 working to a minimal degree

Resource-based API client and oauth stuff is working reasonably

adds api_client and prompt files

Adds base.html file

Doctor welcome and patient check-in

Doctor welcome and patient check-in

Serializer-based approach to interfacing with simple Models

Serializer update/create for appointments. Rename "*Resource" -> "*Endpoint"

docstrings

renames api_client file to endpoints

endpoint docs

sync skeleton

Limits model serializer translation layer

Adds sync layer to glue models to API data through serializers.

basic sync is working

time waiting logic

time waiting logic

sync docstrings

Adds form & view stubs, and some template stubs

Adds form & view stubs, and some template stubs

url & templates

Patient Info form

Template renaming

endpoint testing

endpoint testing

finished minimal api testing

oauth test data

admin tweaks

Doctor appointment list view

Whoami form validation inside form, rather than view

Doctor start/end consult

A bit of bootstrap, and some form changes

sync command

Time waiting in template

Checkin success and such

skipping info update

presentation

Prep for hackathon candidates. Removes form validation, checkin/start-consult methods from model logic, some comments on design constraints, adds a few notes and todos

more tweaks in preparation for the hackathon

more comments

Resolve outdated dependency issues, migrations, and confirm social-auth works.

Remove unused dependency.

add DB initialization script

readme

Merge prompt into readme
  • Loading branch information
mattmerrifield committed May 8, 2018
1 parent d4ec941 commit 613fc01
Show file tree
Hide file tree
Showing 39 changed files with 1,550 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,6 @@ ENV/
.ropeproject

db.sqlite3

# secret environment stuff
docker/environment
6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 61 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,68 @@
# drchrono Hackathon

### Check-in kiosk

Picture going to the doctor's office and replacing the receptionist and paper forms
with a kiosk similar to checking in for a flight.

There should be an account association flow where a doctor can authenticate using
their drchrono account and set up the kiosk for their office.

After the doctor is logged in, a page should be displayed that lets patients check
in. A patient with an appointment should first confirm their identity (first/last
name maybe SSN) then be able to update their demographic information using the
patient chart API endpoint. Once the they have filled out that information the
app can set the appointment status to "Arrived" (Appointment API Docs).

The doctor should also have their own page they can leave open that displays
today’s appointments, indicating which patients have checked in and how long they
have been waiting. From this screen, the doctor can indicate they are seeing a
patient, which stops the “time spent waiting” clock. The doctor should also see
the overall average wait time for all patients they have ever seen.

Outside of these base requirements, you are free to develop any features you think
make sense.

To begin, fork the drchrono API project repo at https://github.com/drchrono/api-example-django

We’ve built this repo to save you some set-up time! It contains a few baseline structural elements for you to build on.
It’s a great starting point, but there are probably some tweaks and improvements to be made before you continue building
out new functionality. It doesn't quite work; it's your job to make it work, and then make it awesome!

Use the drchrono API docs and feel free to reach out to the people operations team with any questions and we'll get back
to you ASAP.

### Requirements
- [pip](https://pip.pypa.io/en/stable/)
- [python virtual env](https://packaging.python.org/installing/#creating-and-using-virtual-environments)
- [docker](https://www.docker.com/community-edition)
- a free [drchrono.com](https://www.drchrono.com/sign-up/) account

### Setup
``` bash
$ pip install -r requirements.txt
$ python manage.py runserver
```

`social_auth_drchrono/` contains a custom provider for [Python Social Auth](http://python-social-auth.readthedocs.io/en/latest/) that handles OAUTH for drchrono. To configure it, set these fields in your `drchrono/settings.py` file:
#### API token
The first thing to do is get an API token from drchrono.com, and connect this local application to it!

This project has `social-auth` preconfigured for you. The `social_auth_drchrono/` contains a custom provider for
[Python Social Auth](http://python-social-auth.readthedocs.io/en/latest/) that handles OAUTH through drchrono. It should
require only minimal configuration.

1) Log in to [drchrono.com](https://www.drchrono.com)
2) Go to the [API management page](https://app.drchrono.com/api-management/)
3) Make a new application
4) Copy the `SOCIAL_AUTH_CLIENT_ID` and `SOCIAL_AUTH_CLIENT_SECRET` to your `docker/environment` file.
5) Set your redirect URI to `http://localhost:8080/complete/drchrono/`


### Dev environment Setup
Docker should take care of all the dependencies for you. It will create two containers: an application server and a
MySQL database server. See `docker-compose.yml` for details.

```
$ git clone [email protected]:drchrono/api-example-django.git hackathon
$ docker-compose up
```
SOCIAL_AUTH_DRCHRONO_KEY
SOCIAL_AUTH_DRCHRONO_SECRET
SOCIAL_AUTH_DRCHRONO_SCOPE
LOGIN_REDIRECT_URL
```

Then connect with a browser to [http://localhost:8080/setup]() and use the web to authorize the application.


### Happy Hacking!
If you have trouble at any point in the setup process, feel free to reach out to the developer
who introduced you to the project. We try to minimize setup friction, but
30 changes: 30 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: '2'
services:
db:
container_name: db
image: mysql:5.7
environment:
- "MYSQL_ALLOW_EMPTY_PASSWORD=True"
ports:
- "3306:3306"
volumes:
- "./docker/data:/docker-entrypoint-initdb.d" # Put data here to be loaded on container creation
working_dir: "/docker-entrypoint-initdb.d"

drchrono:
container_name: drchrono
image: drchrono
env_file:
- "docker/environment" # Any environmental variables you want can go in this plain text file. See the docs.
ports:
- "8080:8080"
# command: /bin/bash -c "while true; do echo mark; sleep 2; done"
command: "python ./manage.py migrate && python ./manage.py runserver 0.0.0.0:8080"
volumes:
- ".:/usr/src/app"
working_dir: /usr/src/app
depends_on:
- "db"
build:
context: .
dockerfile: ./docker/drchrono-dockerfile
1 change: 1 addition & 0 deletions docker/data/init_db.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE DATABASE drchrono;
3 changes: 3 additions & 0 deletions docker/dbsnapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
docker-compose -f $DIR/../docker-compose.yml run db mysqldump drchrono k> init.sql
2 changes: 2 additions & 0 deletions docker/drchrono-dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# onbuild docker image automatically runs pip install on requirements.txt
FROM python:2-onbuild
4 changes: 4 additions & 0 deletions docker/environment.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Get your tokens from the drchrono.com website under Account->API

SOCIAL_AUTH_CLIENT_ID=XXXXXXXXXXXXXXX
SOCIAL_AUTH_SECRET=YYYYYYYYYYYYYYYYYYYYY
23 changes: 23 additions & 0 deletions drchrono/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib import admin

from drchrono.models import Patient, Appointment, Doctor


class PatientAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'date_of_birth', 'social_security_number']


class DoctorAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name']


class AppointmentAdmin(admin.ModelAdmin):
"""
Primary admin interface to view *all* appointments.
"""
list_display = ['patient', 'status', 'scheduled_time', 'checkin_time', 'time_waiting']


admin.site.register(Doctor, DoctorAdmin)
admin.site.register(Patient, PatientAdmin)
admin.site.register(Appointment, AppointmentAdmin)
220 changes: 220 additions & 0 deletions drchrono/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import requests
from social.apps.django_app.default.models import UserSocialAuth
import logging


class APIException(Exception): pass


class Forbidden(APIException): pass


class NotFound(APIException): pass


class Conflict(APIException): pass


ERROR_CODES = {
403: Forbidden,
404: NotFound,
409: Conflict,
}


# TODO: this API abstraction is included for your convenience. If you don't like it, feel free to change it.
class BaseEndpoint(object):
"""
A python wrapper for the basic rules of the drchrono API endpoints.
Provides consistent, pythonic usage and return values from the API.
Abstracts away:
- base URL,
- details of authentication
- list iteration
- response codes
All return values will be dicts, or lists of dicts.
Subclasses should implement a specific endpoint.
"""
BASE_URL = 'https://drchrono.com/api/'
endpoint = ''

def __init__(self):
"""
Creates an API client which will act on behalf of a specific user
"""
self.oauth_provider = UserSocialAuth.objects.get(provider='drchrono')
self.access_token = self.oauth_provider.extra_data['access_token']

@property
def logger(self):
name = "{}.{}".format(__name__, self.endpoint)
return logging.getLogger(name)

def _url(self, id=""):
if id:
id = "/{}".format(id)
return "{}{}{}".format(self.BASE_URL, self.endpoint, id)

def _auth_headers(self, kwargs):
"""
Adds auth information to the kwargs['header'], as expected by get/put/post/delete
Modifies kwargs in place. Returns None.
"""
kwargs['headers'] = kwargs.get('headers', {})
kwargs['headers'].update({
'Authorization': 'Bearer {}'.format(self.access_token),

})

def _json_or_exception(self, response):
"""
returns the JSON content or raises an exception, based on what kind of response (2XX/4XX) we get
"""
if response.ok:
if response.status_code != 204: # No Content
return response.json()
else:
exe = ERROR_CODES.get(response.status_code, APIException)
raise exe(response.content)

def _request(self, method, *args, **kwargs):
# dirty, universal way to use the requests library directly for debugging
url = self._url(kwargs.pop(id, ''))
self._auth_headers(kwargs)
return getattr(requests, method)(url, *args, **kwargs)

def list(self, params=None, **kwargs):
"""
Returns an iterator to retrieve all objects at the specified resource. Waits to exhaust the current page before
retrieving the next, which might result in choppy responses.
"""
self.logger.debug("list()")
url = self._url()
self._auth_headers(kwargs)
# Response will be one page out of a paginated results list
response = requests.get(url, params=params, **kwargs)
if response.ok:
self.logger.debug("list got page {}".format('url'))
while url:
data = response.json()
url = data['next'] # Same as the resource URL, but with the page query parameter present
for result in data['results']:
yield result
else:
exe = ERROR_CODES.get(response.status_code, APIException)
self.logger.debug("list exception {}".format(exe))
raise exe(response.content)
self.logger.debug("list() complete")

def fetch(self, id, params=None, **kwargs):
"""
Retrieve a specific object by ID
"""
url = self._url(id)
self._auth_headers(kwargs)
response = requests.get(url, params=params, **kwargs)
self.logger.info("fetch {}".format(response.status_code))
return self._json_or_exception(response)

def create(self, data=None, json=None, **kwargs):
"""
Used to create an object at a resource with the included values.
Response body will be the requested object, with the ID it was assigned.
Success: 201 (Created)
Failure:
- 400 (Bad Request)
- 403 (Forbidden)
- 409 (Conflict)
"""
url = self._url()
self._auth_headers(kwargs)
response = requests.post(url, data=data, json=json, **kwargs)
return self._json_or_exception(response)

def update(self, id, data, partial=True, **kwargs):
"""
Updates an object. Returns None.
When partial=False, uses PUT to update the entire object at the given ID with the given values.
When partial=TRUE [the default] uses PATCH to update only the given fields on the object.
Response body will be empty.
Success: 204 (No Content)
Failure:
- 400 (Bad Request)
- 403 (Forbidden)
- 409 (Conflict)
"""
url = self._url(id)
self._auth_headers(kwargs)
if partial:
response = requests.patch(url, data, **kwargs)
else:
response = requests.put(url, data, **kwargs)
return self._json_or_exception(response)

def delete(self, id, **kwargs):
"""
Deletes the object at this resource with the given ID.
Response body will be empty.
Success: 204 (No Content)
Failure:
- 400 (Bad Request)
- 403 (Forbidden)
"""
url = self._url(id)
self._auth_headers(kwargs)
response = requests.delete(url)
return self._json_or_exception(response)


class PatientEndpoint(BaseEndpoint):
endpoint = "patients"


class AppointmentEndpoint(BaseEndpoint):
endpoint = "appointments"

# Special parameter requirements for a given resource should be explicitly called out
def list(self, params=None, date=None, start=None, end=None, **kwargs):
"""
List appointments on a given date, or between two dates
"""
# Just parameter parsing & checking
params = params or {}
if start and end:
date_range = "{}/{}".format(start, end)
params['date_range'] = date_range
elif date:
params['date'] = date
if 'date' not in params and 'date_range' not in params:
raise Exception("Must provide either start & end, or date argument")
return super(AppointmentEndpoint, self).list(params, **kwargs)


class DoctorEndpoint(BaseEndpoint):
endpoint = "doctors"

def update(self, id, data, partial=True, **kwargs):
raise NotImplementedError("the API does not allow updating doctors")

def create(self, data=None, json=None, **kwargs):
raise NotImplementedError("the API does not allow creating doctors")

def delete(self, id, **kwargs):
raise NotImplementedError("the API does not allow deleteing doctors")


class AppointmentProfileEndpoint(BaseEndpoint):
endpoint = "appointment_profiles"
Loading

0 comments on commit 613fc01

Please sign in to comment.