-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
d4ec941
commit 613fc01
Showing
39 changed files
with
1,550 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,3 +91,6 @@ ENV/ | |
.ropeproject | ||
|
||
db.sqlite3 | ||
|
||
# secret environment stuff | ||
docker/environment |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
CREATE DATABASE drchrono; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Oops, something went wrong.