diff --git a/.gitignore b/.gitignore index df4b189..0bbc78a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ doc/_build/ doc/output/ api_test.py log/* +synthea_output/ diff --git a/requirements.txt b/requirements.txt index 044be4c..f199714 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ django_js_reverse python-decouple django-cryptography djangorestframework +drf_yasg2 pandas + diff --git a/webservice/apps/api/apps.py b/webservice/apps/api/apps.py index 8a12abb..591460c 100644 --- a/webservice/apps/api/apps.py +++ b/webservice/apps/api/apps.py @@ -9,58 +9,58 @@ class ApiConfig(AppConfig): verbose_name = _('API') verbose_name_plural = _('APIs') - try: - assert settings.SWAGGER_SETTINGS - except AttributeError: - # We only load this setting, if it is not available in the overall settings.py file - settings.SWAGGER_SETTINGS = { - 'SECURITY_DEFINITIONS': { - 'Hawk': { - 'type': 'apiKey', - 'description': 'HTTP Holder-Of-Key Authentication Scheme, https://github.com/hapijs/hawk, https://hawkrest.readthedocs.io/en/latest/
Ex header:
\'Authorization\': \'Hawk mac="F4+S9cu7yZiZEgdtqzMpOOdudvqcV2V2Yzk2WcphECc=", hash="+7fKUX+djeQolvnLTxr0X47e//UHKbkRlajwMw3tx3w=", id="7FI5JET4", ts="1592905433", nonce="DlV-fL"\'', - 'name': 'Authorization', - 'in': 'header' - } - } - } + # try: + # assert settings.SWAGGER_SETTINGS + # except AttributeError: + # # We only load this setting, if it is not available in the overall settings.py file + # settings.SWAGGER_SETTINGS = { + # 'SECURITY_DEFINITIONS': { + # 'Hawk': { + # 'type': 'apiKey', + # 'description': 'HTTP Holder-Of-Key Authentication Scheme, https://github.com/hapijs/hawk, https://hawkrest.readthedocs.io/en/latest/
Ex header:
\'Authorization\': \'Hawk mac="F4+S9cu7yZiZEgdtqzMpOOdudvqcV2V2Yzk2WcphECc=", hash="+7fKUX+djeQolvnLTxr0X47e//UHKbkRlajwMw3tx3w=", id="7FI5JET4", ts="1592905433", nonce="DlV-fL"\'', + # 'name': 'Authorization', + # 'in': 'header' + # } + # } + # } - try: - assert settings.REST_FRAMEWORK - except AttributeError: - # We only load this setting, if it is not available in the overall settings.py file - # To protect all API views with Hawk by default, put this in your settings: - # https://hawkrest.readthedocs.io/en/latest/usage.html#protecting-api-views-with-hawk - settings.REST_FRAMEWORK = { + # try: + # assert settings.REST_FRAMEWORK + # except AttributeError: + # # We only load this setting, if it is not available in the overall settings.py file + # # To protect all API views with Hawk by default, put this in your settings: + # # https://hawkrest.readthedocs.io/en/latest/usage.html#protecting-api-views-with-hawk + # settings.REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'apps.api.authentication.APIHawk', - ), + # 'DEFAULT_AUTHENTICATION_CLASSES': ( + # 'apps.api.authentication.APIHawk', + # ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), + # 'DEFAULT_PERMISSION_CLASSES': ( + # 'rest_framework.permissions.IsAuthenticated', + # ), - # 'DEFAULT_AUTHENTICATION_CLASSES': ( - # 'rest_framework.authentication.TokenAuthentication', - # ), + # # 'DEFAULT_AUTHENTICATION_CLASSES': ( + # # 'rest_framework.authentication.TokenAuthentication', + # # ), - # 'DEFAULT_PERMISSION_CLASSES': ( - # 'rest_framework.permissions.IsAuthenticated', ), + # # 'DEFAULT_PERMISSION_CLASSES': ( + # # 'rest_framework.permissions.IsAuthenticated', ), - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - #'DEFAULT_PERMISSION_CLASSES': [ - # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - #], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 10 - } + # # Use Django's standard `django.contrib.auth` permissions, + # # or allow read-only access for unauthenticated users. + # #'DEFAULT_PERMISSION_CLASSES': [ + # # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + # #], + # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + # 'PAGE_SIZE': 10 + # } - try: - assert settings.HAWK_MESSAGE_EXPIRATION - except AttributeError: - # We only load this setting, if it is not available in the overall settings.py file - settings.HAWK_MESSAGE_EXPIRATION = 60 + # try: + # assert settings.HAWK_MESSAGE_EXPIRATION + # except AttributeError: + # # We only load this setting, if it is not available in the overall settings.py file + # settings.HAWK_MESSAGE_EXPIRATION = 60 def ready(self): from . import signals \ No newline at end of file diff --git a/webservice/apps/api/urls.py b/webservice/apps/api/urls.py index 0a40e1e..9138647 100644 --- a/webservice/apps/api/urls.py +++ b/webservice/apps/api/urls.py @@ -7,24 +7,24 @@ from drf_yasg2 import openapi from . import views -from apps.dropoff.api.views import DatadropViewSet -from apps.invitation.api.views import InvitationViewSet -from apps.researcher.api.views import ResearcherViewSet -from apps.storage.api.views import StorageEngineViewSet, StorageLocationViewSet -from apps.study.api.views import StudyViewSet -from apps.virtual_machine.api.views import (VirtualMachineViewSet, - VirtualMachineOperatingSystemViewSet, - VirtualMachineProfileViewSet, - VirtualMachineMemoryViewSet, - VirtualMachineNetworkViewSet, - VirtualMachineStorageViewSet, - VirtualMachineGPUViewSet) +# from apps.dropoff.api.views import DatadropViewSet +# from apps.invitation.api.views import InvitationViewSet +# from apps.researcher.api.views import ResearcherViewSet +# from apps.storage.api.views import StorageEngineViewSet, StorageLocationViewSet +# from apps.study.api.views import StudyViewSet +# from apps.virtual_machine.api.views import (VirtualMachineViewSet, +# VirtualMachineOperatingSystemViewSet, +# VirtualMachineProfileViewSet, +# VirtualMachineMemoryViewSet, +# VirtualMachineNetworkViewSet, +# VirtualMachineStorageViewSet, +# VirtualMachineGPUViewSet) schema_view = get_schema_view( openapi.Info( - title="Virtual Research Environment API", + title="Synthea WebService API", default_version='v1', - description="Here you can see a list of API endpoints and actions that are available to communicate with the VRE API", + description="Info about Synthea WebServer API", terms_of_service="https://www.rug.nl", contact=openapi.Contact(email="vre_team@rug.nl"), license=openapi.License(name="MIT License"), @@ -33,27 +33,27 @@ schema_view = get_schema_view( permission_classes=(permissions.AllowAny,), ) -api_router_v1 = routers.DefaultRouter() +#api_router_v1 = routers.DefaultRouter() -api_router_v1.register(r'researchers', ResearcherViewSet) +# api_router_v1.register(r'researchers', ResearcherViewSet) -api_router_v1.register(r'studies', StudyViewSet) +# api_router_v1.register(r'studies', StudyViewSet) -api_router_v1.register(r'dropoffs', DatadropViewSet) +# api_router_v1.register(r'dropoffs', DatadropViewSet) -api_router_v1.register(r'invitations', InvitationViewSet) +# api_router_v1.register(r'invitations', InvitationViewSet) -api_router_v1.register(r'storageengines', StorageEngineViewSet) -api_router_v1.register(r'storagelocations', StorageLocationViewSet) +# api_router_v1.register(r'storageengines', StorageEngineViewSet) +# api_router_v1.register(r'storagelocations', StorageLocationViewSet) -# Order is important for virtual machines. Longest match first -api_router_v1.register(r'virtualmachines/profiles', VirtualMachineProfileViewSet) -api_router_v1.register(r'virtualmachines/storage', VirtualMachineStorageViewSet) -api_router_v1.register(r'virtualmachines/memory', VirtualMachineMemoryViewSet) -api_router_v1.register(r'virtualmachines/network', VirtualMachineNetworkViewSet) -api_router_v1.register(r'virtualmachines/gpu', VirtualMachineGPUViewSet) -api_router_v1.register(r'virtualmachines/os', VirtualMachineOperatingSystemViewSet) -api_router_v1.register(r'virtualmachines', VirtualMachineViewSet) +# # Order is important for virtual machines. Longest match first +# api_router_v1.register(r'virtualmachines/profiles', VirtualMachineProfileViewSet) +# api_router_v1.register(r'virtualmachines/storage', VirtualMachineStorageViewSet) +# api_router_v1.register(r'virtualmachines/memory', VirtualMachineMemoryViewSet) +# api_router_v1.register(r'virtualmachines/network', VirtualMachineNetworkViewSet) +# api_router_v1.register(r'virtualmachines/gpu', VirtualMachineGPUViewSet) +# api_router_v1.register(r'virtualmachines/os', VirtualMachineOperatingSystemViewSet) +# api_router_v1.register(r'virtualmachines', VirtualMachineViewSet) # Main namespace for the API urls app_name = 'api' @@ -66,6 +66,10 @@ urlpatterns = [ # Also this will give the full url to the OpenAPI documentation path('info/', views.Info.as_view(), name='info'), + path('states/', views.States.as_view(), name='states'), + path('modules/', views.Modules.as_view(), name='modules'), + path('generate/', views.Generate.as_view(), name='generate'), + # Add extra namespace for versioning the API - path('v1/', include((api_router_v1.urls,'api'),namespace='v1')), + #path('v1/', include((api_router_v1.urls,'api'),namespace='v1')), ] \ No newline at end of file diff --git a/webservice/apps/api/views.py b/webservice/apps/api/views.py index 0049650..9c1491a 100644 --- a/webservice/apps/api/views.py +++ b/webservice/apps/api/views.py @@ -1,11 +1,23 @@ from rest_framework.views import APIView from rest_framework.response import Response +from django.http import FileResponse + +from django.views.decorators.csrf import csrf_exempt + from rest_framework.decorators import schema +from rest_framework import status from django.urls import reverse from lib.utils.general import get_ip_address +from apps.synthea.api.serializers import SyntheaSerializer + +from apps.synthea.lib.utils import available_states, available_modules,run_synthea + +import mimetypes + + @schema(None) class Info(APIView): """ @@ -49,4 +61,37 @@ class Info(APIView): except AttributeError: pass - return Response(data) \ No newline at end of file + return Response(data) + + +#@schema(None) +class States(APIView): + + def get(self, request, format=None): + return Response(available_states()) + + +class Modules(APIView): + + def get(self, request, format=None): + return Response(available_modules()) + +class Generate(APIView): + + def post(self, request, format=None): + print('Post data') + print(request.data) + + serializer = SyntheaSerializer(data=request.data) + if serializer.is_valid(): + synthea = serializer.save() + zipfile = synthea.generate() + print(zipfile) + response = FileResponse(zipfile.open('rb'), content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={zipfile.name}' + + return response + + + #return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/webservice/apps/synthea/api/serializers.py b/webservice/apps/synthea/api/serializers.py new file mode 100644 index 0000000..460c287 --- /dev/null +++ b/webservice/apps/synthea/api/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from apps.synthea.models import Synthea + +class SyntheaSerializer(serializers.ModelSerializer): + class Meta: + model = Synthea + fields = ['state', 'population', 'gender', 'age', 'module'] \ No newline at end of file diff --git a/webservice/apps/synthea/apps.py b/webservice/apps/synthea/apps.py index 4ea0616..bfe5f91 100644 --- a/webservice/apps/synthea/apps.py +++ b/webservice/apps/synthea/apps.py @@ -7,4 +7,43 @@ class SyntheaConfig(AppConfig): name = 'apps.synthea' label = 'synthea' verbose_name = _('Synthea') - verbose_name_plural = _('Synthea') \ No newline at end of file + verbose_name_plural = _('Synthea') + + try: + assert settings.SYNTHEA_BASE_DIR + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_BASE_DIR = settings.BASE_DIR / '../synthea' + + try: + assert settings.SYNTHEA_OUTPUT_DIR + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_OUTPUT_DIR = settings.BASE_DIR / '../synthea_output' + + try: + assert settings.SYNTHEA_MODULE_DIR + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_MODULE_DIR = settings.SYNTHEA_BASE_DIR / 'src/main/resources/modules/' + + + try: + assert settings.SYNTHEA_RESOURCE_DIR + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_RESOURCE_DIR = settings.SYNTHEA_BASE_DIR / 'src/main/resources/' + + try: + assert settings.SYNTHEA_STATES_DIR + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_STATES_DIR = settings.SYNTHEA_BASE_DIR / 'src/main/resources/geography/' + + + + try: + assert settings.SYNTHEA_EXPORT_TYPE + except AttributeError: + # We only load this setting, if it is not available in the overall settings.py file + settings.SYNTHEA_EXPORT_TYPE = 'fhir_stu3' \ No newline at end of file diff --git a/webservice/apps/synthea/forms.py b/webservice/apps/synthea/forms.py index 01fbc7c..78d7ec6 100644 --- a/webservice/apps/synthea/forms.py +++ b/webservice/apps/synthea/forms.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext_lazy as _ + from django.forms import ModelForm from django import forms @@ -12,16 +14,16 @@ class SyntheaForm(ModelForm): fields = ['state', 'population', 'gender', 'age', 'module'] # This is loaded only once during startup. So changing the state data will not be picked up after a restart - state_options = [('','Any')] + state_options = [('',_('Select'))] for item in available_states(): - state_options.append((item,item)) + state_options.append((item['id'],item['name'])) - module_options = [('','Any')] + module_options = [('',_('Any'))] for item in available_modules(): - module_options.append((item['module'],item['name'])) + module_options.append((item['id'],item['name'])) widgets = { 'state': forms.Select(choices=state_options), - 'gender': forms.Select(choices=[('','Any'),('m','Male'),('f','Female')]), + 'gender': forms.Select(choices=[('',_('Any')),('m',_('Male')),('f',_('Female'))]), 'module': forms.Select(choices=module_options) } \ No newline at end of file diff --git a/webservice/apps/synthea/lib/utils.py b/webservice/apps/synthea/lib/utils.py index ab0d59e..73a1044 100644 --- a/webservice/apps/synthea/lib/utils.py +++ b/webservice/apps/synthea/lib/utils.py @@ -5,36 +5,41 @@ import subprocess from zipfile import ZipFile import json -def available_states(): - #TODO: Make a setting for this path - location = Path('/opt/development/synthea_webservice/synthea/src/main/resources/geography/') +from django.conf import settings +from uuid import uuid4 - df = pd.read_csv(location / 'timezones.csv', index_col=False) - # The state information is expected in the first column - states = df[df.columns[0]].to_list() - states.sort() + +def available_states(): + states = [] + # Read the timezones.csv file from the Synthea resources. This should give us all the 'state' on the first column + df = pd.read_csv(settings.SYNTHEA_STATES_DIR / 'timezones.csv', index_col=False) + for state in df[df.columns[0]].to_list(): + states.append({'id' : state , 'name' : state}) + + #states = df[df.columns[0]].to_list() + # Sort on name + states = sorted(states, key=lambda k: k['name'].lower()) + #states.sort() return states def available_modules(): - #TODO: Make a setting for this path - location = Path('/opt/development/synthea_webservice/synthea/src/main/resources/modules/') - - # Assumption here: A folder is a single module. And all .json in the main modules folder is a module. + # Assumption here: Only .json files in the main folder are modules. The rest are submodules... modules = [] - for module in location.iterdir(): + for module in settings.SYNTHEA_MODULE_DIR.iterdir(): if module.is_file() and module.suffix == '.json': data = json.loads(module.read_text()) - modules.append({'module' : module.name.replace('.json',''), 'name' : data['name']}) + modules.append({'id' : module.name.replace('.json',''), 'name' : data['name']}) modules = sorted(modules, key=lambda k: k['name'].lower()) return modules def run_synthea(state = None, population = None, gender = None, age = None, module = None): - # TODO: Make synthea setting(s) - location = '/opt/development/synthea_webservice/synthea/' - synthea_cmd = ['/opt/development/synthea_webservice/synthea/run_synthea'] + # Add a unique dir to the output, so multiple Synthea processes can run parallel + temp_id = uuid4().hex + output_folder = settings.SYNTHEA_OUTPUT_DIR / temp_id + + synthea_cmd = [settings.SYNTHEA_BASE_DIR / 'run_synthea','--exporter.baseDirectory',output_folder] zip_file = 'Synthea_' - zip_export = location if population: synthea_cmd.append('-p') @@ -62,7 +67,7 @@ def run_synthea(state = None, population = None, gender = None, age = None, modu process_ok = False log = '' - with subprocess.Popen(synthea_cmd,cwd=location, stdout=subprocess.PIPE,stderr=subprocess.PIPE) as process: + with subprocess.Popen(synthea_cmd,cwd=settings.SYNTHEA_BASE_DIR, stdout=subprocess.PIPE,stderr=subprocess.PIPE) as process: for line in process.stdout: line = line.decode('utf8') log += line @@ -70,14 +75,10 @@ def run_synthea(state = None, population = None, gender = None, age = None, modu process_ok = line.find('BUILD SUCCESSFUL') >= 0 if process_ok: - with ZipFile(f'{zip_export}/{zip_file}.zip', 'w') as export: - for file in Path(location + 'output/fhir_stu3').iterdir(): + with ZipFile(f'{output_folder}/{zip_file}_{temp_id}.zip', 'w') as export: + for file in (output_folder / settings.SYNTHEA_EXPORT_TYPE).iterdir(): export.write(file,file.name) - return Path(f'{zip_export}/{zip_file}.zip') + return (log,Path(f'{output_folder}/{zip_file}_{temp_id}.zip')) else: - raise Exception(log) - - - - + raise Exception(log) \ No newline at end of file diff --git a/webservice/apps/synthea/migrations/0002_auto_20201116_1457.py b/webservice/apps/synthea/migrations/0002_auto_20201116_1457.py new file mode 100644 index 0000000..09bc913 --- /dev/null +++ b/webservice/apps/synthea/migrations/0002_auto_20201116_1457.py @@ -0,0 +1,38 @@ +# Generated by Django 3.1.3 on 2020-11-16 13:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('synthea', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='synthea', + name='log', + field=models.TextField(blank=True, help_text='Synthea logfile output', verbose_name='Log'), + ), + migrations.AlterField( + model_name='synthea', + name='age', + field=models.CharField(blank=True, default='18-100', help_text='Select the age range. Enter [min age]-[max age]', max_length=10, verbose_name='Age range'), + ), + migrations.AlterField( + model_name='synthea', + name='gender', + field=models.CharField(blank=True, help_text='Select the gender type', max_length=1, verbose_name='Gender'), + ), + migrations.AlterField( + model_name='synthea', + name='module', + field=models.CharField(blank=True, help_text='Select the module', max_length=50, verbose_name='Module'), + ), + migrations.AlterField( + model_name='synthea', + name='state', + field=models.CharField(help_text='The state for which synthea generate data.', max_length=200, verbose_name='State'), + ), + ] diff --git a/webservice/apps/synthea/models.py b/webservice/apps/synthea/models.py index 8892f2d..755f071 100644 --- a/webservice/apps/synthea/models.py +++ b/webservice/apps/synthea/models.py @@ -7,6 +7,8 @@ from django_cryptography.fields import encrypt from lib.utils.general import get_random_string from lib.models.base import MetaDataModel +from .lib.utils import run_synthea + import uuid # Create your models here. @@ -18,7 +20,25 @@ class Synthea(MetaDataModel): id = models.UUIDField(_('ID'), primary_key=True, unique=True, default=uuid.uuid4, editable=False, help_text=_('A unique id')) state = models.CharField(_('State'), max_length=200, help_text=_('The state for which synthea generate data.')) - population = models.PositiveSmallIntegerField(_('Population'), blank=True, default=50, help_text=_('The size of the population')) + population = models.PositiveSmallIntegerField(_('Population'), default=50, help_text=_('The size of the population')) gender = models.CharField(_('Gender'), blank=True,max_length=1, help_text=_('Select the gender type')) - age = models.CharField(_('Age range'), blank=True,max_length=10, help_text=_('Select the age range')) + age = models.CharField(_('Age range'), blank=True,default='18-100', max_length=10, help_text=_('Select the age range. Enter [min age]-[max age]')) module = models.CharField(_('Module'),blank=True, max_length=50, help_text=_('Select the module')) + log = models.TextField(_('Log'),blank=True, help_text=_('Synthea logfile output')) + + + def generate(self): + log,zip_file = run_synthea( + self.state, + self.population, + self.gender, + self.age, + self.module + ) + + self.log = log + + self.save() + + return zip_file + diff --git a/webservice/apps/synthea/templates/synthea/generator_form.html b/webservice/apps/synthea/templates/synthea/generator_form.html index 3aafae7..965505a 100644 --- a/webservice/apps/synthea/templates/synthea/generator_form.html +++ b/webservice/apps/synthea/templates/synthea/generator_form.html @@ -9,7 +9,7 @@ {% endblock %} {% block content %} -

Synthea generartor

+

Synthea Generator

Enter the form fields and press submit. U vraagt, wij draaien ;)

{% csrf_token %} diff --git a/webservice/apps/synthea/templates/synthea/index.html b/webservice/apps/synthea/templates/synthea/index.html index 5e83ce1..9cf31d8 100644 --- a/webservice/apps/synthea/templates/synthea/index.html +++ b/webservice/apps/synthea/templates/synthea/index.html @@ -11,4 +11,5 @@ {% block content %}

dHealt Nederland

Onder leiding van UMCG

+

Github branches: https://github.com/dHealthNL

{% endblock %} diff --git a/webservice/apps/synthea/views.py b/webservice/apps/synthea/views.py index a43e0bc..5b1ffc9 100644 --- a/webservice/apps/synthea/views.py +++ b/webservice/apps/synthea/views.py @@ -1,6 +1,7 @@ from django.shortcuts import render from django.http import HttpResponse from apps.synthea.forms import SyntheaForm +from apps.synthea.models import Synthea from .lib.utils import run_synthea @@ -21,24 +22,13 @@ def show_synthea_form(request): # check whether it's valid: if form.is_valid(): # process the data in form.cleaned_data as required + synthea = form.save() + zipfile = synthea.generate() - try: - zipfile = run_synthea( - form.cleaned_data['state'], - form.cleaned_data['population'], - form.cleaned_data['gender'], - form.cleaned_data['age'], - form.cleaned_data['module'] - ) - - mime_type, _ = mimetypes.guess_type(zipfile) - response = HttpResponse(zipfile.open('rb'), content_type=mime_type) - response['Content-Disposition'] = f'attachment; filename={zipfile.name}' + response = HttpResponse(zipfile.open('rb'), content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={zipfile.name}' - return response - - except Exception as ex: - print(ex) + return response # if a GET (or any other method) we'll create a blank form else: diff --git a/webservice/webservice/settings.py b/webservice/webservice/settings.py index 2aee057..ad2f85d 100644 --- a/webservice/webservice/settings.py +++ b/webservice/webservice/settings.py @@ -41,6 +41,8 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django_js_reverse', + 'rest_framework', + 'drf_yasg2', 'apps.api', 'apps.RUG_template', diff --git a/webservice/webservice/urls.py b/webservice/webservice/urls.py index ba9474e..ab8c2b9 100644 --- a/webservice/webservice/urls.py +++ b/webservice/webservice/urls.py @@ -20,6 +20,9 @@ from django_js_reverse.views import urls_js urlpatterns = [ path('admin/', admin.site.urls), + + # Add API urls for all the known models + path('api/', include('apps.api.urls')), path('', include('apps.synthea.urls')),