diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..df4b189
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+venv/*
+.vscode/*
+static/*
+__pycache__
+db.sqlite3
+doc/_build/
+doc/output/
+api_test.py
+log/*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..37bf3ce
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "synthea"]
+ path = synthea
+ url = https://github.com/dHealthNL/synthea.git
+[submodule "synthea-international"]
+ path = synthea-international
+ url = https://github.com/dHealthNL/synthea-international.git
+[submodule "synthea-modules"]
+ path = synthea-modules
+ url = https://github.com/dHealthNL/synthea-modules.git
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..044be4c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+Django
+dj-database-url
+django_js_reverse
+python-decouple
+django-cryptography
+djangorestframework
+pandas
diff --git a/synthea b/synthea
new file mode 160000
index 0000000..16129ad
--- /dev/null
+++ b/synthea
@@ -0,0 +1 @@
+Subproject commit 16129ad4fbef18e5c89b941e1b14a8fcf04abf07
diff --git a/synthea-international b/synthea-international
new file mode 160000
index 0000000..da67258
--- /dev/null
+++ b/synthea-international
@@ -0,0 +1 @@
+Subproject commit da67258b54ee83e3d4ad11b5f0dfcbc5bb053e34
diff --git a/synthea-modules b/synthea-modules
new file mode 160000
index 0000000..824b4d3
--- /dev/null
+++ b/synthea-modules
@@ -0,0 +1 @@
+Subproject commit 824b4d3182434bd20ef644a21a8bc4a828f399dc
diff --git a/webservice/apps/RUG_template/__init__.py b/webservice/apps/RUG_template/__init__.py
new file mode 100644
index 0000000..4d348bc
--- /dev/null
+++ b/webservice/apps/RUG_template/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'apps.RUG_template.apps.RugTemplateConfig'
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/admin.py b/webservice/apps/RUG_template/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/webservice/apps/RUG_template/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/webservice/apps/RUG_template/apps.py b/webservice/apps/RUG_template/apps.py
new file mode 100644
index 0000000..71c96ae
--- /dev/null
+++ b/webservice/apps/RUG_template/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+class RugTemplateConfig(AppConfig):
+ name = 'apps.RUG_template'
+ label = 'RUG_template'
+ verbose_name = _('RUG Template')
+ verbose_name_plural = _('RUG Template')
diff --git a/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.mo b/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..71cbdf3
Binary files /dev/null and b/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.mo differ
diff --git a/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.po b/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..04a7191
--- /dev/null
+++ b/webservice/apps/RUG_template/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,213 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-07-30 15:42+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: apps/RUG_template/apps.py:7 apps/RUG_template/apps.py:8
+msgid "RUG Template"
+msgstr ""
+
+#: apps/RUG_template/templates/400.html:4
+#: apps/RUG_template/templates/400.html:5
+msgid "Oops, sorry, bad request (400)"
+msgstr ""
+
+#: apps/RUG_template/templates/400.html:7
+#: apps/RUG_template/templates/404.html:7
+msgid ""
+"Unfortunately, the link to this page does not work, the page (temporarily) "
+"does not exist or it has been moved."
+msgstr ""
+
+#: apps/RUG_template/templates/403.html:4
+#: apps/RUG_template/templates/403.html:5
+msgid "Oops, sorry, forbidden access (403)"
+msgstr ""
+
+#: apps/RUG_template/templates/403.html:7
+msgid "Unfortunately, you do not have rights to access this url."
+msgstr ""
+
+#: apps/RUG_template/templates/404.html:4
+#: apps/RUG_template/templates/404.html:5
+msgid "Oops, sorry, page not found (404)"
+msgstr ""
+
+#: apps/RUG_template/templates/500.html:4
+#: apps/RUG_template/templates/500.html:5
+msgid "Oops, sorry, server error (500)"
+msgstr ""
+
+#: apps/RUG_template/templates/500.html:7
+msgid "Unfortunately, something went wrong on the server."
+msgstr ""
+
+#: apps/RUG_template/templates/admin/base_site.html:24
+msgid "Language"
+msgstr ""
+
+#: apps/RUG_template/templates/admin/base_site.html:32
+msgid "Documentation"
+msgstr ""
+
+#: apps/RUG_template/templates/admin/base_site.html:36
+msgid "Change password"
+msgstr ""
+
+#: apps/RUG_template/templates/admin/base_site.html:38
+msgid "Log out"
+msgstr ""
+
+#: apps/RUG_template/templates/base.html:7
+msgid "Welcome at RUG"
+msgstr ""
+
+#: apps/RUG_template/templates/base.html:113
+msgid "Language selection"
+msgstr ""
+
+#: apps/RUG_template/templates/index.html:4
+#: apps/RUG_template/templates/index.html:5
+msgid "Welcome to the RUG Template page"
+msgstr ""
+
+#: apps/RUG_template/templates/index.html:7
+msgid "Simple RUG Template"
+msgstr ""
+
+#: apps/RUG_template/templates/index.html:8
+msgid "Some more text"
+msgstr ""
+
+#: apps/RUG_template/templates/menu.html:4
+msgid "Section"
+msgstr ""
+
+#: apps/RUG_template/templates/menu.html:7
+msgid "Menu item"
+msgstr ""
+
+#: apps/RUG_template/templates/pager.html:13
+msgid "previous"
+msgstr ""
+
+#: apps/RUG_template/templates/pager.html:21
+msgid "next"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/login.html:4
+#: apps/RUG_template/templates/registration/login.html:5
+#: apps/RUG_template/templates/registration/login.html:8
+#: apps/RUG_template/templates/registration/login.html:18
+msgid "Login"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/login.html:10
+msgid ""
+"You can login here to create your schedules. If you do not have a login, "
+"please contact: some_one@rug.nl"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/login.html:24
+msgid "Lost password?"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_complete.html:4
+#: apps/RUG_template/templates/registration/password_reset_complete.html:5
+#: apps/RUG_template/templates/registration/password_reset_complete.html:7
+msgid "Password reset complete"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_complete.html:9
+#, python-format
+msgid ""
+"Your new password has been set. You can log in now on the log in page."
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:4
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:5
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:7
+msgid "Set a new password!"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:9
+msgid "Here you can set a new password."
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:17
+msgid "Change my password"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:23
+msgid ""
+"The password reset link was invalid, possibly because it has already been "
+"used. Please request a new password reset."
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_done.html:4
+#: apps/RUG_template/templates/registration/password_reset_done.html:5
+#: apps/RUG_template/templates/registration/password_reset_done.html:8
+msgid "Reset password, email sent"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_done.html:11
+msgid ""
+"We've emailed you instructions for setting your password. You should receive "
+"the email shortly!"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_email.html:2
+#, python-format
+msgid ""
+"You're receiving this email because you requested a password reset for your "
+"user account at %(site_name)s.\n"
+"\n"
+"Please go to the following page and choose a new password:\n"
+"\n"
+"%(protocol)s://%(domain)s%(reset_url)s\n"
+"\n"
+"Your username, in case you've forgotten: %(user)s\n"
+"\n"
+"Thanks for using our site!\n"
+"\n"
+"The %(site_name)s team"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:4
+#: apps/RUG_template/templates/registration/password_reset_form.html:5
+#: apps/RUG_template/templates/registration/password_reset_form.html:8
+msgid "Reset password"
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:10
+msgid ""
+"Here you can request a password reset. Please enter your email address that "
+"is used for registration."
+msgstr ""
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:17
+msgid "Reset my password"
+msgstr ""
+
+#: apps/RUG_template/templates/singup.html:4
+#: apps/RUG_template/templates/singup.html:5
+#: apps/RUG_template/templates/singup.html:7
+#: apps/RUG_template/templates/singup.html:14
+msgid "Singup"
+msgstr ""
diff --git a/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.mo b/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6a1957d
Binary files /dev/null and b/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.po b/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..24389c3
--- /dev/null
+++ b/webservice/apps/RUG_template/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,240 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-07-30 15:42+0200\n"
+"PO-Revision-Date: 2020-05-27 15:59+0200\n"
+"Last-Translator: Joshua Rubingh \n"
+"Language-Team: \n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 2.0.6\n"
+
+#: apps/RUG_template/apps.py:7 apps/RUG_template/apps.py:8
+msgid "RUG Template"
+msgstr "RUG Template"
+
+#: apps/RUG_template/templates/400.html:4
+#: apps/RUG_template/templates/400.html:5
+msgid "Oops, sorry, bad request (400)"
+msgstr "Oeps, sorry, pagina niet gevonden (404)"
+
+#: apps/RUG_template/templates/400.html:7
+#: apps/RUG_template/templates/404.html:7
+msgid ""
+"Unfortunately, the link to this page does not work, the page (temporarily) "
+"does not exist or it has been moved."
+msgstr ""
+"Helaas werkt de link naar deze pagina niet, de pagina bestaat (tijdelijk) "
+"niet of is verplaatst."
+
+#: apps/RUG_template/templates/403.html:4
+#: apps/RUG_template/templates/403.html:5
+msgid "Oops, sorry, forbidden access (403)"
+msgstr "Oeps, sorry, geen toegang (403)"
+
+#: apps/RUG_template/templates/403.html:7
+msgid "Unfortunately, you do not have rights to access this url."
+msgstr "Helaas heb je geen rechten om toegang te krijgen tot deze URL."
+
+#: apps/RUG_template/templates/404.html:4
+#: apps/RUG_template/templates/404.html:5
+msgid "Oops, sorry, page not found (404)"
+msgstr "Oeps, sorry, pagina niet gevonden (404)"
+
+#: apps/RUG_template/templates/500.html:4
+#: apps/RUG_template/templates/500.html:5
+msgid "Oops, sorry, server error (500)"
+msgstr "Oeps, sorry, server error (500)"
+
+#: apps/RUG_template/templates/500.html:7
+msgid "Unfortunately, something went wrong on the server."
+msgstr "Helaas is er iets misgegaan op de server."
+
+#: apps/RUG_template/templates/admin/base_site.html:24
+msgid "Language"
+msgstr "Taal"
+
+#: apps/RUG_template/templates/admin/base_site.html:32
+msgid "Documentation"
+msgstr "Documentatie"
+
+#: apps/RUG_template/templates/admin/base_site.html:36
+msgid "Change password"
+msgstr "Verander wachtwoord"
+
+#: apps/RUG_template/templates/admin/base_site.html:38
+msgid "Log out"
+msgstr "Logouit"
+
+#: apps/RUG_template/templates/base.html:7
+msgid "Welcome at RUG"
+msgstr "Welkom bij de RUG"
+
+#: apps/RUG_template/templates/base.html:113
+msgid "Language selection"
+msgstr "Taal keuze"
+
+#: apps/RUG_template/templates/index.html:4
+#: apps/RUG_template/templates/index.html:5
+msgid "Welcome to the RUG Template page"
+msgstr "Welkom bij de RUG Template pagina"
+
+#: apps/RUG_template/templates/index.html:7
+msgid "Simple RUG Template"
+msgstr "Simpel RUG Template"
+
+#: apps/RUG_template/templates/index.html:8
+msgid "Some more text"
+msgstr "Nog wat tekst"
+
+#: apps/RUG_template/templates/menu.html:4
+msgid "Section"
+msgstr "Sectie"
+
+#: apps/RUG_template/templates/menu.html:7
+msgid "Menu item"
+msgstr "Menu item"
+
+#: apps/RUG_template/templates/pager.html:13
+msgid "previous"
+msgstr "vorige"
+
+#: apps/RUG_template/templates/pager.html:21
+msgid "next"
+msgstr "volgende"
+
+#: apps/RUG_template/templates/registration/login.html:4
+#: apps/RUG_template/templates/registration/login.html:5
+#: apps/RUG_template/templates/registration/login.html:8
+#: apps/RUG_template/templates/registration/login.html:18
+msgid "Login"
+msgstr "Login"
+
+#: apps/RUG_template/templates/registration/login.html:10
+msgid ""
+"You can login here to create your schedules. If you do not have a login, "
+"please contact: some_one@rug.nl"
+msgstr ""
+"Hier kunt u inloggen om nieuwe roosters te maken. Als je geen login hebt "
+"neem dan contact op met iemand@rug.nl"
+
+#: apps/RUG_template/templates/registration/login.html:24
+msgid "Lost password?"
+msgstr "Wachtwoord kwijt?"
+
+#: apps/RUG_template/templates/registration/password_reset_complete.html:4
+#: apps/RUG_template/templates/registration/password_reset_complete.html:5
+#: apps/RUG_template/templates/registration/password_reset_complete.html:7
+msgid "Password reset complete"
+msgstr "Wachtwoord reset is kompleet"
+
+#: apps/RUG_template/templates/registration/password_reset_complete.html:9
+#, python-format
+msgid ""
+"Your new password has been set. You can log in now on the log in page."
+msgstr ""
+"Je nieuwe wachtwoord is ingesteld. Je kunt nu inloggen via de inlog pagina."
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:4
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:5
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:7
+msgid "Set a new password!"
+msgstr "Stel een nieuw wachtwoord in!"
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:9
+msgid "Here you can set a new password."
+msgstr "Hier kun je een nieuw wachtwoord instellen."
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:17
+msgid "Change my password"
+msgstr "Verander mijn wachtwoord"
+
+#: apps/RUG_template/templates/registration/password_reset_confirm.html:23
+msgid ""
+"The password reset link was invalid, possibly because it has already been "
+"used. Please request a new password reset."
+msgstr ""
+"De link voor het opnieuw instellen van het wachtwoord was ongeldig, mogelijk "
+"omdat deze al is gebruikt. Vraag een nieuwe reset van het wachtwoord aan."
+
+#: apps/RUG_template/templates/registration/password_reset_done.html:4
+#: apps/RUG_template/templates/registration/password_reset_done.html:5
+#: apps/RUG_template/templates/registration/password_reset_done.html:8
+msgid "Reset password, email sent"
+msgstr "Wachtwoord is gereset, email is verstuurd"
+
+#: apps/RUG_template/templates/registration/password_reset_done.html:11
+msgid ""
+"We've emailed you instructions for setting your password. You should receive "
+"the email shortly!"
+msgstr ""
+"We hebben u een e-mail gestuurd met instructies voor het instellen van uw "
+"wachtwoord. U ontvangt de e-mail binnenkort!"
+
+#: apps/RUG_template/templates/registration/password_reset_email.html:2
+#, python-format
+msgid ""
+"You're receiving this email because you requested a password reset for your "
+"user account at %(site_name)s.\n"
+"\n"
+"Please go to the following page and choose a new password:\n"
+"\n"
+"%(protocol)s://%(domain)s%(reset_url)s\n"
+"\n"
+"Your username, in case you've forgotten: %(user)s\n"
+"\n"
+"Thanks for using our site!\n"
+"\n"
+"The %(site_name)s team"
+msgstr ""
+"Je ontvangt deze e-mail omdat je een wachtwoord reset hebt aangevraagd voor "
+"je gebruikersaccount op%(site_name)s.\n"
+"\n"
+"Ga naar de volgende pagina en kies een nieuw wachtwoord:\n"
+"\n"
+"%(protocol)s://%(domain)s%(reset_url)s\n"
+"\n"
+"Uw gebruikersnaam, voor het geval u het bent vergeten: %(user)s\n"
+"\n"
+"Bedankt voor het gebruiken van onze site!\n"
+"\n"
+"Het team van %(site_name)s"
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:4
+#: apps/RUG_template/templates/registration/password_reset_form.html:5
+#: apps/RUG_template/templates/registration/password_reset_form.html:8
+msgid "Reset password"
+msgstr "Reset wachtwoord"
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:10
+msgid ""
+"Here you can request a password reset. Please enter your email address that "
+"is used for registration."
+msgstr ""
+"Hier kunt u een wachtwoord reset aanvragen. Voer uw e-mailadres in dat wordt "
+"gebruikt voor registratie."
+
+#: apps/RUG_template/templates/registration/password_reset_form.html:17
+msgid "Reset my password"
+msgstr "Reset mijn wachtwoord"
+
+#: apps/RUG_template/templates/singup.html:4
+#: apps/RUG_template/templates/singup.html:5
+#: apps/RUG_template/templates/singup.html:7
+#: apps/RUG_template/templates/singup.html:14
+msgid "Singup"
+msgstr "Opgeven"
+
+#~ msgid "Password reset"
+#~ msgstr "Wachtwoord reset"
diff --git a/webservice/apps/RUG_template/middleware.py b/webservice/apps/RUG_template/middleware.py
new file mode 100644
index 0000000..cf9f7d3
--- /dev/null
+++ b/webservice/apps/RUG_template/middleware.py
@@ -0,0 +1,38 @@
+import pytz
+import requests
+
+from ipware import get_client_ip
+from django.utils import timezone
+
+# make sure you add `TimezoneMiddleware` appropriately in settings.py: 'apps.RUG_template.middleware.TimezoneMiddleware'
+class TimezoneMiddleware:
+ """ Middleware to check user timezone. """
+ def __init__(self, get_response):
+ self.get_response = get_response
+ # One-time configuration and initialization.
+
+ def __call__(self, request):
+ # Code to be executed for each request before
+ # the view (and later middleware) are called.
+ client_ip, is_routable = get_client_ip(request)
+ user_time_zone = request.session.get('user_time_zone', None)
+ try:
+ if user_time_zone is None and is_routable and client_ip is not None:
+ # Here we use an online service to get visitor info. Maybe not the nicest way to do it, but it is a way
+ # Also we only check when we get a public IP address. Local networks will not be checked online
+ # https://freegeoip.app
+ freegeoip_response = requests.get('https://freegeoip.app/json/{0}'.format(client_ip))
+ freegeoip_response_json = freegeoip_response.json()
+ user_time_zone = freegeoip_response_json['time_zone']
+ if user_time_zone:
+ request.session['user_time_zone'] = user_time_zone
+ timezone.activate(pytz.timezone(user_time_zone))
+ except:
+ pass
+
+ response = self.get_response(request)
+
+ # Code to be executed for each request/response after
+ # the view is called.
+
+ return response
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/migrations/__init__.py b/webservice/apps/RUG_template/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/apps/RUG_template/models.py b/webservice/apps/RUG_template/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/webservice/apps/RUG_template/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/RUG_Logo.jpg b/webservice/apps/RUG_template/static/RUG_template/images/RUG_Logo.jpg
new file mode 100644
index 0000000..da3037b
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/RUG_Logo.jpg differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/add-icon.png b/webservice/apps/RUG_template/static/RUG_template/images/add-icon.png
new file mode 100644
index 0000000..81295f3
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/add-icon.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/edit-icon.png b/webservice/apps/RUG_template/static/RUG_template/images/edit-icon.png
new file mode 100644
index 0000000..2873781
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/edit-icon.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/encrypted_document.png b/webservice/apps/RUG_template/static/RUG_template/images/encrypted_document.png
new file mode 100644
index 0000000..f28e7f0
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/encrypted_document.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/eye-closed.png b/webservice/apps/RUG_template/static/RUG_template/images/eye-closed.png
new file mode 100644
index 0000000..a4e0b3c
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/eye-closed.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/eye-open.png b/webservice/apps/RUG_template/static/RUG_template/images/eye-open.png
new file mode 100644
index 0000000..f977f19
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/eye-open.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/favicon.ico b/webservice/apps/RUG_template/static/RUG_template/images/favicon.ico
new file mode 100644
index 0000000..bc12218
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/favicon.ico differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/flag-en.png b/webservice/apps/RUG_template/static/RUG_template/images/flag-en.png
new file mode 100644
index 0000000..9ab58e1
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/flag-en.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/flag-nl.png b/webservice/apps/RUG_template/static/RUG_template/images/flag-nl.png
new file mode 100644
index 0000000..bafed87
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/flag-nl.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/minus-icon.png b/webservice/apps/RUG_template/static/RUG_template/images/minus-icon.png
new file mode 100644
index 0000000..a94d7dd
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/minus-icon.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/plus-icon.png b/webservice/apps/RUG_template/static/RUG_template/images/plus-icon.png
new file mode 100644
index 0000000..6b90d3b
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/plus-icon.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/images/send_email.png b/webservice/apps/RUG_template/static/RUG_template/images/send_email.png
new file mode 100644
index 0000000..4134f6f
Binary files /dev/null and b/webservice/apps/RUG_template/static/RUG_template/images/send_email.png differ
diff --git a/webservice/apps/RUG_template/static/RUG_template/javascript/humanize-duration.min.js b/webservice/apps/RUG_template/static/RUG_template/javascript/humanize-duration.min.js
new file mode 100644
index 0000000..8caa889
--- /dev/null
+++ b/webservice/apps/RUG_template/static/RUG_template/javascript/humanize-duration.min.js
@@ -0,0 +1 @@
+!function(){var n={y:function(n){return 1===n?"χρόνος":"χρόνια"},mo:function(n){return 1===n?"μήνας":"μήνες"},w:function(n){return 1===n?"εβδομάδα":"εβδομάδες"},d:function(n){return 1===n?"μέρα":"μέρες"},h:function(n){return 1===n?"ώρα":"ώρες"},m:function(n){return 1===n?"λεπτό":"λεπτά"},s:function(n){return 1===n?"δευτερόλεπτο":"δευτερόλεπτα"},ms:function(n){return 1===n?"χιλιοστό του δευτερολέπτου":"χιλιοστά του δευτερολέπτου"},decimal:","},y={ar:{y:function(n){return 1===n?"سنة":"سنوات"},mo:function(n){return 1===n?"شهر":"أشهر"},w:function(n){return 1===n?"أسبوع":"أسابيع"},d:function(n){return 1===n?"يوم":"أيام"},h:function(n){return 1===n?"ساعة":"ساعات"},m:function(n){return 2').text('Toggle password').insertAfter(value);
+ });
+
+ jQuery('button.password_toggle').on('click',function(event){
+ event.preventDefault();
+ toggle_password(this);
+ });
+}
+
+function toggle_password(button) {
+ button = jQuery(button);
+ let password_field = button.prev('input');
+ let show = password_field.attr('type') == 'password';
+
+ password_field.attr('type',( show ? 'text' : 'password' ));
+ button.removeClass('password_hidden password_shown').addClass(( show ? 'password_shown' : 'password_hidden' ))
+}
+
+function human_sizes(value) {
+ const units = ['B','KB','MB','GB','TB','HB'];
+ const unit_value = 1000;
+
+ let counter = 0;
+ while (value / unit_value > 1) {
+ value /= unit_value;
+ counter++;
+ }
+ return value + '' + units[counter];
+}
+
+function label_required_fields() {
+ jQuery('input,textarea,select').filter('[required]:visible').each(function(counter,value){
+ let field = jQuery(value);
+ jQuery('label[for="' + field.attr('id') + '"]').append('*');
+ });
+
+ /*
+ jQuery('select').each(function(counter,value){
+ let field = jQuery(value);
+ console.log(jQuery('label[for="' + field.attr('id') + '"]'));
+ jQuery('label[for="' + field.attr('id') + '"]').append('*');
+ });
+ */
+}
+
+jQuery(function(){
+ jQuery.ajaxSetup({
+ beforeSend: function(xhr, settings) {
+ if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+ xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
+ }
+ }
+ });
+
+ init_password_toggles();
+ label_required_fields();
+});
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.formset.js b/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.formset.js
new file mode 100644
index 0000000..e352fe8
--- /dev/null
+++ b/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.formset.js
@@ -0,0 +1,252 @@
+/**
+ * https://raw.githubusercontent.com/elo80ka/django-dynamic-formset/master/src/jquery.formset.js
+ * jQuery Formset 1.5-pre
+ * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
+ * @requires jQuery 1.2.6 or later
+ *
+ * Copyright (c) 2009, Stanislaus Madueke
+ * All rights reserved.
+ *
+ * Licensed under the New BSD License
+ * See: http://www.opensource.org/licenses/bsd-license.php
+ */
+;(function($) {
+ $.fn.formset = function(opts)
+ {
+ var options = $.extend({}, $.fn.formset.defaults, opts),
+ flatExtraClasses = options.extraClasses.join(' '),
+ totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
+ maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
+ minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'),
+ childElementSelector = 'input,select,textarea,label,div',
+ $$ = $(this),
+
+ applyExtraClasses = function(row, ndx) {
+ if (options.extraClasses) {
+ row.removeClass(flatExtraClasses);
+ row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
+ }
+ },
+
+ updateElementIndex = function(elem, prefix, ndx) {
+ var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
+ replacement = prefix + '-' + ndx + '-';
+ if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
+ if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
+ if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
+ },
+
+ hasChildElements = function(row) {
+ return row.find(childElementSelector).length > 0;
+ },
+
+ showAddButton = function() {
+ return maxForms.length == 0 || // For Django versions pre 1.2
+ (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
+ },
+
+ /**
+ * Indicates whether delete link(s) can be displayed - when total forms > min forms
+ */
+ showDeleteLinks = function() {
+ return minForms.length == 0 || // For Django versions pre 1.7
+ (minForms.val() == '' || (totalForms.val() - minForms.val() > 0));
+ },
+
+ insertDeleteLink = function(row) {
+ var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
+ addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
+
+ var delButtonHTML = '' + options.deleteText +'';
+ if (options.deleteContainerClass) {
+ // If we have a specific container for the remove button,
+ // place it as the last child of that container:
+ row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML);
+ } else if (row.is('TR')) {
+ // If the forms are laid out in table rows, insert
+ // the remove button into the last table cell:
+ row.children(':last').append(delButtonHTML);
+ } else if (row.is('UL') || row.is('OL')) {
+ // If they're laid out as an ordered/unordered list,
+ // insert an
after the last list item:
+ row.append('
' + delButtonHTML + '
');
+ } else {
+ // Otherwise, just insert the remove button as the
+ // last child element of the form's container:
+ row.append(delButtonHTML);
+ }
+
+ // Check if we're under the minimum number of forms - not to display delete link at rendering
+ if (!showDeleteLinks()){
+ row.find('a.' + delCssSelector).hide();
+ }
+
+ row.find('a.' + delCssSelector).click(function() {
+ var row = $(this).parents('.' + options.formCssClass),
+ del = row.find('input:hidden[id $= "-DELETE"]'),
+ buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
+ forms;
+ if (del.length) {
+ // We're dealing with an inline formset.
+ // Rather than remove this form from the DOM, we'll mark it as deleted
+ // and hide it, then let Django handle the deleting:
+ del.val('on');
+ row.hide();
+ forms = $('.' + options.formCssClass).not(':hidden');
+ totalForms.val(forms.length);
+ } else {
+ row.remove();
+ // Update the TOTAL_FORMS count:
+ forms = $('.' + options.formCssClass).not('.formset-custom-template');
+ totalForms.val(forms.length);
+ }
+ for (var i=0, formCount=forms.length; i');
+ row.hide();
+ } else {
+ del.before('');
+ }
+ // Hide any labels associated with the DELETE checkbox:
+ $('label[for="' + del.attr('id') + '"]').hide();
+ del.remove();
+ }
+ if (hasChildElements(row)) {
+ row.addClass(options.formCssClass);
+ if (row.is(':visible')) {
+ insertDeleteLink(row);
+ applyExtraClasses(row, i);
+ }
+ }
+ });
+
+ if ($$.length) {
+ var hideAddButton = !showAddButton(),
+ addButton, template;
+ if (options.formTemplate) {
+ // If a form template was specified, we'll clone it to generate new form instances:
+ template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
+ template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
+ template.find(childElementSelector).each(function() {
+ updateElementIndex($(this), options.prefix, '__prefix__');
+ });
+ insertDeleteLink(template);
+ } else {
+ // Otherwise, use the last form in the formset; this works much better if you've got
+ // extra (>= 1) forms (thnaks to justhamade for pointing this out):
+ if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide();
+ template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
+ template.find('input:hidden[id $= "-DELETE"]').remove();
+ // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
+ template.find(childElementSelector).not(options.keepFieldValues).each(function() {
+ var elem = $(this);
+ // If this is a checkbox or radiobutton, uncheck it.
+ // This fixes Issue 1, reported by Wilson.Andrew.J:
+ if (elem.is('input:checkbox') || elem.is('input:radio')) {
+ elem.attr('checked', false);
+ } else {
+ elem.val('');
+ }
+ });
+ }
+ // FIXME: Perhaps using $.data would be a better idea?
+ options.formTemplate = template;
+
+ var addButtonHTML = '' + options.addText + '';
+ if (options.addContainerClass) {
+ // If we have a specific container for the "add" button,
+ // place it as the last child of that container:
+ var addContainer = $('[class*="' + options.addContainerClass + '"');
+ addContainer.append(addButtonHTML);
+ addButton = addContainer.find('[class="' + options.addCssClass + '"]');
+ } else if ($$.is('TR')) {
+ // If forms are laid out as table rows, insert the
+ // "add" button in a new table row:
+ var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
+ buttonRow = $('
' + addButtonHTML + '
').addClass(options.formCssClass + '-add');
+ $$.parent().append(buttonRow);
+ addButton = buttonRow.find('a');
+ } else {
+ // Otherwise, insert it immediately after the last form:
+ $$.filter(':last').after(addButtonHTML);
+ addButton = $$.filter(':last').next();
+ }
+
+ if (hideAddButton) addButton.hide();
+
+ addButton.click(function() {
+ var formCount = parseInt(totalForms.val()),
+ row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
+ buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this),
+ delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.');
+ applyExtraClasses(row, formCount);
+ row.insertBefore(buttonRow).show();
+ row.find(childElementSelector).each(function() {
+ updateElementIndex($(this), options.prefix, formCount);
+ });
+ totalForms.val(formCount + 1);
+ // Check if we're above the minimum allowed number of forms -> show all delete link(s)
+ if (showDeleteLinks()){
+ $('a.' + delCssSelector).each(function(){$(this).show();});
+ }
+ // Check if we've exceeded the maximum allowed number of forms:
+ if (!showAddButton()) buttonRow.hide();
+ // If a post-add callback was supplied, call it with the added form:
+ if (options.added) options.added(row);
+ return false;
+ });
+ }
+
+ return $$;
+ };
+
+ /* Setup plugin defaults */
+ $.fn.formset.defaults = {
+ prefix: 'form', // The form prefix for your django formset
+ formTemplate: null, // The jQuery selection cloned to generate new form instances
+ addText: 'add another', // Text for the add link
+ deleteText: 'remove', // Text for the delete link
+ addContainerClass: null, // Container CSS class for the add link
+ deleteContainerClass: null, // Container CSS class for the delete link
+ addCssClass: 'add-row', // CSS class applied to the add link
+ deleteCssClass: 'delete-row', // CSS class applied to the delete link
+ formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
+ extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
+ keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
+ added: null, // Function called each time a new form is added
+ removed: null, // Function called each time a form is deleted
+ hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button)
+ };
+})(jQuery);
+
diff --git a/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.tablesorter.min.js b/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.tablesorter.min.js
new file mode 100644
index 0000000..50a0e17
--- /dev/null
+++ b/webservice/apps/RUG_template/static/RUG_template/javascript/jquery.tablesorter.min.js
@@ -0,0 +1 @@
+!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&"object"==typeof module.exports?module.exports=e(require("jquery")):e(jQuery)}(function(e){return function(A){"use strict";var L=A.tablesorter={version:"2.31.3",parsers:[],widgets:[],defaults:{theme:"default",widthFixed:!1,showProcessing:!1,headerTemplate:"{content}",onRenderTemplate:null,onRenderHeader:null,cancelSelection:!0,tabIndex:!0,dateFormat:"mmddyyyy",sortMultiSortKey:"shiftKey",sortResetKey:"ctrlKey",usNumberFormat:!0,delayInit:!1,serverSideSorting:!1,resort:!0,headers:{},ignoreCase:!0,sortForce:null,sortList:[],sortAppend:null,sortStable:!1,sortInitialOrder:"asc",sortLocaleCompare:!1,sortReset:!1,sortRestart:!1,emptyTo:"bottom",stringTo:"max",duplicateSpan:!0,textExtraction:"basic",textAttribute:"data-text",textSorter:null,numberSorter:null,initWidgets:!0,widgetClass:"widget-{name}",widgets:[],widgetOptions:{zebra:["even","odd"]},initialized:null,tableClass:"",cssAsc:"",cssDesc:"",cssNone:"",cssHeader:"",cssHeaderRow:"",cssProcessing:"",cssChildRow:"tablesorter-childRow",cssInfoBlock:"tablesorter-infoOnly",cssNoSort:"tablesorter-noSort",cssIgnoreRow:"tablesorter-ignoreRow",cssIcon:"tablesorter-icon",cssIconNone:"",cssIconAsc:"",cssIconDesc:"",cssIconDisabled:"",pointerClick:"click",pointerDown:"mousedown",pointerUp:"mouseup",selectorHeaders:"> thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:!1,headerList:[],empties:{},strings:{},parsers:[],globalize:0,imgAttr:0},css:{table:"tablesorter",cssHasChild:"tablesorter-hasChildRow",childRow:"tablesorter-childRow",colgroup:"tablesorter-colgroup",header:"tablesorter-header",headerRow:"tablesorter-headerRow",headerIn:"tablesorter-header-inner",icon:"tablesorter-icon",processing:"tablesorter-processing",sortAsc:"tablesorter-headerAsc",sortDesc:"tablesorter-headerDesc",sortNone:"tablesorter-headerUnSorted"},language:{sortAsc:"Ascending sort applied, ",sortDesc:"Descending sort applied, ",sortNone:"No sort applied, ",sortDisabled:"sorting is disabled",nextAsc:"activate to apply an ascending sort",nextDesc:"activate to apply a descending sort",nextNone:"activate to remove the sort"},regex:{templateContent:/\{content\}/g,templateIcon:/\{icon\}/g,templateName:/\{name\}/i,spaces:/\s+/g,nonWord:/\W/g,formElements:/(input|select|button|textarea)/i,chunk:/(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,chunks:/(^\\0|\\0$)/,hex:/^0x[0-9a-f]+$/i,comma:/,/g,digitNonUS:/[\s|\.]/g,digitNegativeTest:/^\s*\([.\d]+\)/,digitNegativeReplace:/^\s*\(([.\d]+)\)/,digitTest:/^[\-+(]?\d+[)]?$/,digitReplace:/[,.'"\s]/g},string:{max:1,min:-1,emptymin:1,emptymax:-1,zero:0,none:0,"null":0,top:!0,bottom:!1},keyCodes:{enter:13},dates:{},instanceMethods:{},setup:function(t,r){if(t&&t.tHead&&0!==t.tBodies.length&&!0!==t.hasInitialized){var e,o="",s=A(t),a=A.metadata;t.hasInitialized=!1,t.isProcessing=!0,t.config=r,A.data(t,"tablesorter",r),L.debug(r,"core")&&(console[console.group?"group":"log"]("Initializing tablesorter v"+L.version),A.data(t,"startoveralltimer",new Date)),r.supportsDataObject=((e=A.fn.jquery.split("."))[0]=parseInt(e[0],10),1':"",d.$headers=A(A.map(d.$table.find(d.selectorHeaders),function(e,t){var r,o,s,a,n,i=A(e);if(!L.getClosest(i,"tr").hasClass(d.cssIgnoreRow))return/(th|td)/i.test(e.nodeName)||(n=L.getClosest(i,"th, td"),i.attr("data-column",n.attr("data-column"))),r=L.getColumnData(d.table,d.headers,t,!0),d.headerContent[t]=i.html(),""===d.headerTemplate||i.find("."+L.css.headerIn).length||(a=d.headerTemplate.replace(L.regex.templateContent,i.html()).replace(L.regex.templateIcon,i.find("."+L.css.icon).length?"":l),d.onRenderTemplate&&(o=d.onRenderTemplate.apply(i,[t,a]))&&"string"==typeof o&&(a=o),i.html('
+{% endif %}
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/templates/registration/login.html b/webservice/apps/RUG_template/templates/registration/login.html
new file mode 100644
index 0000000..8545c9a
--- /dev/null
+++ b/webservice/apps/RUG_template/templates/registration/login.html
@@ -0,0 +1,25 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "Login" %}{% endblock %}
+{% block pagetitle %}{% trans "Login" %}{% endblock %}
+{% block content %}
+
+ {% trans "Login" %}
+
+ {% blocktrans %}You can login here to create your schedules. If you do not have a login, please contact: some_one@rug.nl{% endblocktrans %}
+
+
+
+{# Assumes you setup the password_reset view in your URLconf #}
+
+{% trans "Reset password, email sent" %}
+
+
+{% blocktrans %}We've emailed you instructions for setting your password. You should receive the email shortly!{% endblocktrans %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/templates/registration/password_reset_email.html b/webservice/apps/RUG_template/templates/registration/password_reset_email.html
new file mode 100644
index 0000000..4106994
--- /dev/null
+++ b/webservice/apps/RUG_template/templates/registration/password_reset_email.html
@@ -0,0 +1,13 @@
+{% load i18n %}{% url 'password_reset_confirm' uidb64=uid token=token as reset_url%}
+{% autoescape off %}{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.
+
+Please go to the following page and choose a new password:
+
+{{ protocol}}://{{ domain }}{{ reset_url }}
+
+Your username, in case you've forgotten: {{ user }}
+
+Thanks for using our site!
+
+The {{ site_name }} team{% endblocktrans %}
+{% endautoescape %}
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/templates/registration/password_reset_form.html b/webservice/apps/RUG_template/templates/registration/password_reset_form.html
new file mode 100644
index 0000000..362f89b
--- /dev/null
+++ b/webservice/apps/RUG_template/templates/registration/password_reset_form.html
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "Reset password" %}{% endblock %}
+{% block pagetitle %}{% trans "Reset password" %}{% endblock %}
+{% block content %}
+
+{% trans "Reset password" %}
+
+{% blocktrans %}Here you can request a password reset. Please enter your email address that is used for registration.{% endblocktrans %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/templates/singup.html b/webservice/apps/RUG_template/templates/singup.html
new file mode 100644
index 0000000..2e6ff1a
--- /dev/null
+++ b/webservice/apps/RUG_template/templates/singup.html
@@ -0,0 +1,18 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "Singup" %}{% endblock %}
+{% block pagetitle %}{% trans "Singup" %}{% endblock %}
+{% block content %}
+
{% trans "Singup" %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/tests.py b/webservice/apps/RUG_template/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/webservice/apps/RUG_template/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/webservice/apps/RUG_template/urls.py b/webservice/apps/RUG_template/urls.py
new file mode 100644
index 0000000..6a0d1d2
--- /dev/null
+++ b/webservice/apps/RUG_template/urls.py
@@ -0,0 +1,7 @@
+from django.urls import path, include
+
+from . import views
+
+urlpatterns = [
+ path('', views.index, name='index'),
+]
\ No newline at end of file
diff --git a/webservice/apps/RUG_template/views.py b/webservice/apps/RUG_template/views.py
new file mode 100644
index 0000000..8832c3a
--- /dev/null
+++ b/webservice/apps/RUG_template/views.py
@@ -0,0 +1,7 @@
+from django.shortcuts import render
+
+# Create your views here.
+def index(request):
+ template_name = 'index.html'
+
+ return render(request, template_name, {})
\ No newline at end of file
diff --git a/webservice/apps/api/__init__.py b/webservice/apps/api/__init__.py
new file mode 100644
index 0000000..b536550
--- /dev/null
+++ b/webservice/apps/api/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'apps.api.apps.ApiConfig'
\ No newline at end of file
diff --git a/webservice/apps/api/admin.py b/webservice/apps/api/admin.py
new file mode 100644
index 0000000..726805c
--- /dev/null
+++ b/webservice/apps/api/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+from .models import Token
+
+@admin.register(Token)
+class TokenAdmin(admin.ModelAdmin):
+ list_display = ('key', 'user','is_supertoken', 'last_access')
+ ordering = ('-last_access', 'user', )
+ search_fields = ('key', 'user__username',)
+ readonly_fields = ('created_at', 'updated_at')
diff --git a/webservice/apps/api/apps.py b/webservice/apps/api/apps.py
new file mode 100644
index 0000000..8a12abb
--- /dev/null
+++ b/webservice/apps/api/apps.py
@@ -0,0 +1,66 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+from django.conf import settings
+
+class ApiConfig(AppConfig):
+ name = 'apps.api'
+ label = 'api'
+ 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.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_PERMISSION_CLASSES': (
+ 'rest_framework.permissions.IsAuthenticated',
+ ),
+
+ # 'DEFAULT_AUTHENTICATION_CLASSES': (
+ # 'rest_framework.authentication.TokenAuthentication',
+ # ),
+
+ # '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
+ }
+
+ 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/authentication.py b/webservice/apps/api/authentication.py
new file mode 100644
index 0000000..11f4cac
--- /dev/null
+++ b/webservice/apps/api/authentication.py
@@ -0,0 +1,72 @@
+# import the logging library
+import logging
+# Get an instance of a logger
+logger = logging.getLogger(__name__)
+
+import django.utils
+from rest_framework import exceptions
+from hawkrest import HawkAuthentication
+
+from .models import Token
+class APIHawk(HawkAuthentication):
+ """This is the API authentication that is using the HAWK authentication mechanism.
+
+ This class will implement a custom credentials and user lookups so that we can dynamically add new users and update tokens.
+ """
+ def hawk_credentials_lookup(self, id):
+ """This method will perform the check if the used token is an existing/known token in the database. This will not lookup a user. Only an existing token.
+
+ Args:
+ id (string): The token key to lookup in the database for existing token.
+
+ Raises:
+ exceptions.AuthenticationFailed: If the given token does not exists.
+
+ Returns:
+ dict: The dictionary holds the token id, the token secret and the used hashing algoritem that is used.
+ """
+ try:
+ token = Token.objects.get(key=id)
+ except Token.DoesNotExist:
+ logger.warning('Requested to validate with invalid/non existing token: {}'.format(id))
+ raise exceptions.AuthenticationFailed('No such token: {}'.format(id))
+
+ return {
+ 'id' : id,
+ 'key' : token.secret,
+ 'algorithm' : 'sha256'
+ }
+
+ def hawk_user_lookup(self, request, credentials):
+ """Return the user account that is connected to the used token.
+
+ Args:
+ request ([type]): The incoming HTTP/API request
+ credentials (dict): The credentials from ~hawk_credentials_lookup
+
+ Raises:
+ exceptions.AuthenticationFailed: If the given token does not exists to an existing user
+
+ Returns:
+ tuple: Returns a tuple holding the user as first item
+ """
+ user = None
+ try:
+ user = Token.objects.get(key=credentials['id']).user
+ except Token.DoesNotExist:
+ logger.warning('Requested to validate non existing user: {}'.format(id))
+ raise exceptions.AuthenticationFailed('No user for token: {}'.format(credentials['id']))
+
+ # Update the date time stamp to now for last access data
+ user.token.last_access = django.utils.timezone.now()
+ user.token.save()
+
+ return (user,None)
+
+ def __repr__(self):
+ """Authentication identifier.
+
+ Returns:
+ string: Returns the name of the used authentication mechanism.
+ """
+ return 'Hawk authenticator'
\ No newline at end of file
diff --git a/webservice/apps/api/locale/en/LC_MESSAGES/django.mo b/webservice/apps/api/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..71cbdf3
Binary files /dev/null and b/webservice/apps/api/locale/en/LC_MESSAGES/django.mo differ
diff --git a/webservice/apps/api/locale/en/LC_MESSAGES/django.po b/webservice/apps/api/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a227bee
--- /dev/null
+++ b/webservice/apps/api/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-07-30 15:42+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: apps/api/apps.py:9
+msgid "API"
+msgstr ""
+
+#: apps/api/apps.py:10
+msgid "APIs"
+msgstr ""
+
+#: apps/api/models.py:28
+msgid "token"
+msgstr ""
+
+#: apps/api/models.py:29
+msgid "tokens"
+msgstr ""
+
+#: apps/api/models.py:31
+msgid "Select the user for this token"
+msgstr ""
+
+#: apps/api/models.py:32
+msgid "Key"
+msgstr ""
+
+#: apps/api/models.py:32
+msgid "The key for this token. This is used for Hawk verification."
+msgstr ""
+
+#: apps/api/models.py:33
+msgid "Secret"
+msgstr ""
+
+#: apps/api/models.py:33
+msgid "The secret for this token. This is used for Hawk signing."
+msgstr ""
+
+#: apps/api/models.py:34
+msgid "Last access"
+msgstr ""
+
+#: apps/api/models.py:34
+msgid "The date and time when this token is last used."
+msgstr ""
+
+#: apps/api/models.py:44
+msgid "Super token"
+msgstr ""
diff --git a/webservice/apps/api/locale/nl/LC_MESSAGES/django.mo b/webservice/apps/api/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6414806
Binary files /dev/null and b/webservice/apps/api/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/webservice/apps/api/locale/nl/LC_MESSAGES/django.po b/webservice/apps/api/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..071a7b3
--- /dev/null
+++ b/webservice/apps/api/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-07-30 15:42+0200\n"
+"PO-Revision-Date: 2020-05-27 16:25+0200\n"
+"Last-Translator: Joshua Rubingh \n"
+"Language-Team: \n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 2.0.6\n"
+
+#: apps/api/apps.py:9
+msgid "API"
+msgstr "API"
+
+#: apps/api/apps.py:10
+msgid "APIs"
+msgstr "APIs"
+
+#: apps/api/models.py:28
+msgid "token"
+msgstr ""
+
+#: apps/api/models.py:29
+msgid "tokens"
+msgstr ""
+
+#: apps/api/models.py:31
+msgid "Select the user for this token"
+msgstr ""
+
+#: apps/api/models.py:32
+msgid "Key"
+msgstr ""
+
+#: apps/api/models.py:32
+msgid "The key for this token. This is used for Hawk verification."
+msgstr ""
+
+#: apps/api/models.py:33
+msgid "Secret"
+msgstr ""
+
+#: apps/api/models.py:33
+msgid "The secret for this token. This is used for Hawk signing."
+msgstr ""
+
+#: apps/api/models.py:34
+msgid "Last access"
+msgstr ""
+
+#: apps/api/models.py:34
+msgid "The date and time when this token is last used."
+msgstr ""
+
+#: apps/api/models.py:44
+msgid "Super token"
+msgstr ""
diff --git a/webservice/apps/api/migrations/0001_initial.py b/webservice/apps/api/migrations/0001_initial.py
new file mode 100644
index 0000000..fd23d06
--- /dev/null
+++ b/webservice/apps/api/migrations/0001_initial.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.0.8 on 2020-07-30 14:15
+
+import apps.api.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django_cryptography.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this model has been created', verbose_name='Date created')),
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this model has been updated', verbose_name='Date updated')),
+ ('key', models.CharField(default=apps.api.models.get_random_key, help_text='The key for this token. This is used for Hawk verification.', max_length=16, unique=True, verbose_name='Key')),
+ ('secret', django_cryptography.fields.encrypt(models.CharField(default=apps.api.models.get_random_secret, help_text='The secret for this token. This is used for Hawk signing.', max_length=64, verbose_name='Secret'))),
+ ('last_access', models.DateTimeField(auto_now_add=True, help_text='The date and time when this token is last used.', verbose_name='Last access')),
+ ('user', models.OneToOneField(help_text='Select the user for this token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'token',
+ 'verbose_name_plural': 'tokens',
+ },
+ ),
+ ]
diff --git a/webservice/apps/api/migrations/__init__.py b/webservice/apps/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/apps/api/models.py b/webservice/apps/api/models.py
new file mode 100644
index 0000000..3603aef
--- /dev/null
+++ b/webservice/apps/api/models.py
@@ -0,0 +1,70 @@
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from django_cryptography.fields import encrypt
+
+from lib.utils.general import get_random_string
+from lib.models.base import MetaDataModel
+
+
+def get_random_key():
+ return get_random_string(8)
+
+def get_random_secret():
+ return get_random_string(32)
+
+class TokenManager(models.Manager):
+ """
+ Custom queryset which will prefetch related user table data when requesting a token from the database as the user is mostly needed every time the token is requested.
+ """
+
+ def get_queryset(self):
+ return super(TokenManager, self).get_queryset().select_related('user')
+
+class Token(MetaDataModel):
+ """Token model that holds all the tokens that are used for the API authentication.
+
+ A new token is generated every time when a new user is created. So there is no need for manual token creating. This is done through a signal :attr:`~apps.api.signals.create_user_token`
+
+ Attributes
+ ----------
+ user : :class:`~django.contrib.auth.models.User`
+ The user to which this token belongs too
+ key : str
+ The key value that is used for token lookups
+ secret : str
+ The secret that is used for encrypting/signing the API messages
+ last_access : datetime
+ The date and time when the token is last used (logged in)
+ """
+
+ class Meta:
+ verbose_name = _('token')
+ verbose_name_plural = _('tokens')
+
+ user = models.OneToOneField(User, on_delete=models.CASCADE, help_text=_('Select the user for this token'))
+ key = models.CharField(_('Key') , unique=True, default=get_random_key, max_length=16, help_text=_('The key for this token. This is used for Hawk verification.'))
+ secret = encrypt(models.CharField(_('Secret') ,max_length=64, default=get_random_secret, help_text=_('The secret for this token. This is used for Hawk signing.')))
+ last_access = models.DateTimeField(_('Last access'),auto_now_add=True, help_text=_('The date and time when this token is last used.'))
+
+ # Custom manager that will retrieve the related user table as well.
+ objects = TokenManager()
+
+ def is_supertoken(self):
+ """Boolean check if the token is belonging to a user with super user rights. Then this token is a super token.
+
+ Returns:
+ bool: Returns true when the token belongs to a super user.
+ """
+ # TODO: Is it allowed to be a super user and researcher? Could give conflict of interests. With the API token you can read other researchers data...
+ return self.user.is_superuser == True
+
+ is_supertoken.boolean = True
+ is_supertoken.short_description = _('Super token')
+
+ def __str__(self):
+ """
+ Print the full name of the researcher based on the first and last name fields of the User model.
+ """
+ return '{} ({})'.format(self.key,self.user.get_full_name())
\ No newline at end of file
diff --git a/webservice/apps/api/signals.py b/webservice/apps/api/signals.py
new file mode 100644
index 0000000..e4e4c8a
--- /dev/null
+++ b/webservice/apps/api/signals.py
@@ -0,0 +1,24 @@
+from django.conf import settings
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from .models import Token
+
+@receiver(post_save, sender=settings.AUTH_USER_MODEL)
+def create_user_token(sender, instance=None, created=False, **kwargs):
+ """
+ When a new user is created, this signal will also create a new API token for this user. So every user will have an API token.
+
+ Arguments
+ ----------
+ sender : sender
+ The model that has triggered the signal
+
+ instance: :attr:`~django.contrib.auth.models.User`
+ The newly created user model data
+
+ created : boolean
+ Wether the object was created (True) or updated (False).
+ """
+ if created:
+ Token.objects.create(user=instance)
diff --git a/webservice/apps/api/tests.py b/webservice/apps/api/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/webservice/apps/api/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/webservice/apps/api/urls.py b/webservice/apps/api/urls.py
new file mode 100644
index 0000000..0a40e1e
--- /dev/null
+++ b/webservice/apps/api/urls.py
@@ -0,0 +1,71 @@
+from django.urls import path, re_path, include
+
+from rest_framework import permissions, routers
+
+from drf_yasg2.views import get_schema_view
+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)
+
+schema_view = get_schema_view(
+ openapi.Info(
+ title="Virtual Research Environment 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",
+ terms_of_service="https://www.rug.nl",
+ contact=openapi.Contact(email="vre_team@rug.nl"),
+ license=openapi.License(name="MIT License"),
+ ),
+ public=True,
+ permission_classes=(permissions.AllowAny,),
+)
+
+api_router_v1 = routers.DefaultRouter()
+
+api_router_v1.register(r'researchers', ResearcherViewSet)
+
+api_router_v1.register(r'studies', StudyViewSet)
+
+api_router_v1.register(r'dropoffs', DatadropViewSet)
+
+api_router_v1.register(r'invitations', InvitationViewSet)
+
+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)
+
+# Main namespace for the API urls
+app_name = 'api'
+urlpatterns = [
+ re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
+ path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
+ path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
+
+ # Extra /api/info path for checking if the Hawk authentication is working.
+ # Also this will give the full url to the OpenAPI documentation
+ path('info/', views.Info.as_view(), name='info'),
+
+ # Add extra namespace for versioning the API
+ 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
new file mode 100644
index 0000000..0049650
--- /dev/null
+++ b/webservice/apps/api/views.py
@@ -0,0 +1,52 @@
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework.decorators import schema
+
+from django.urls import reverse
+
+from lib.utils.general import get_ip_address
+
+@schema(None)
+class Info(APIView):
+ """
+ Show some API information. Also this can be used to check if the Hawk credentials are working.
+
+ Make sure your request does contain the header 'Content-Type': 'application/json'
+ """
+
+ def get(self, request, format=None):
+ """
+ Default API get action will return the following information in a dict:
+
+ - Connected user
+ - Used authentication scheme
+ - The remote IP of the connection
+ - The used content type
+ - The full url to the API documentation (OpenAPI)
+ - If a super token is used
+ """
+
+ data = {
+ 'type' : 'anonymous',
+ 'auth' : 'none',
+ 'remote_ip' : get_ip_address(request),
+ 'content_type' : request.content_type,
+ 'openapi' : request.build_absolute_uri(reverse('api:schema-redoc')),
+ }
+
+ if request.user.is_authenticated:
+
+ data['user'] = request.user.username
+ data['type'] = 'authenticated'
+ data['auth'] = str(request.successful_authenticator)
+
+ if request.user.token.is_supertoken:
+ data['type'] = 'supertoken'
+ else:
+ try:
+ assert request.user.researcher
+ data['type'] = 'researcher'
+ except AttributeError:
+ pass
+
+ return Response(data)
\ No newline at end of file
diff --git a/webservice/apps/synthea/__init__.py b/webservice/apps/synthea/__init__.py
new file mode 100644
index 0000000..8ba84a1
--- /dev/null
+++ b/webservice/apps/synthea/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'apps.synthea.apps.SyntheaConfig'
\ No newline at end of file
diff --git a/webservice/apps/synthea/admin.py b/webservice/apps/synthea/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/webservice/apps/synthea/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/webservice/apps/synthea/apps.py b/webservice/apps/synthea/apps.py
new file mode 100644
index 0000000..4ea0616
--- /dev/null
+++ b/webservice/apps/synthea/apps.py
@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+from django.conf import settings
+
+class SyntheaConfig(AppConfig):
+ name = 'apps.synthea'
+ label = 'synthea'
+ verbose_name = _('Synthea')
+ verbose_name_plural = _('Synthea')
\ No newline at end of file
diff --git a/webservice/apps/synthea/forms.py b/webservice/apps/synthea/forms.py
new file mode 100644
index 0000000..01fbc7c
--- /dev/null
+++ b/webservice/apps/synthea/forms.py
@@ -0,0 +1,27 @@
+from django.forms import ModelForm
+from django import forms
+
+from apps.synthea.models import Synthea
+
+from .lib.utils import available_states, available_modules
+
+class SyntheaForm(ModelForm):
+
+ class Meta:
+ model = Synthea
+ 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')]
+ for item in available_states():
+ state_options.append((item,item))
+
+ module_options = [('','Any')]
+ for item in available_modules():
+ module_options.append((item['module'],item['name']))
+
+ widgets = {
+ 'state': forms.Select(choices=state_options),
+ '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
new file mode 100644
index 0000000..ab0d59e
--- /dev/null
+++ b/webservice/apps/synthea/lib/utils.py
@@ -0,0 +1,83 @@
+from pathlib import Path
+import pandas as pd
+import shlex
+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/')
+
+ 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()
+ 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.
+ modules = []
+ for module in location.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 = 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']
+ zip_file = 'Synthea_'
+ zip_export = location
+
+ if population:
+ synthea_cmd.append('-p')
+ synthea_cmd.append(str(population))
+ zip_file += f'population_{population}_'
+
+ if gender:
+ synthea_cmd.append('-g')
+ synthea_cmd.append(gender.upper())
+ zip_file += f'gender_{gender}_'
+
+ if age:
+ synthea_cmd.append('-a')
+ synthea_cmd.append(age)
+ zip_file += f'age_{age}_'
+
+ if module:
+ synthea_cmd.append('-m')
+ synthea_cmd.append(module)
+ zip_file += f'module_{module}_'
+
+ if state:
+ synthea_cmd.append(state)
+ zip_file += f'state_{state}'
+
+ process_ok = False
+ log = ''
+ with subprocess.Popen(synthea_cmd,cwd=location, stdout=subprocess.PIPE,stderr=subprocess.PIPE) as process:
+ for line in process.stdout:
+ line = line.decode('utf8')
+ log += line
+ if not process_ok:
+ 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():
+ export.write(file,file.name)
+
+ return Path(f'{zip_export}/{zip_file}.zip')
+ else:
+ raise Exception(log)
+
+
+
+
diff --git a/webservice/apps/synthea/migrations/0001_initial.py b/webservice/apps/synthea/migrations/0001_initial.py
new file mode 100644
index 0000000..3221990
--- /dev/null
+++ b/webservice/apps/synthea/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1.3 on 2020-11-13 09:36
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Synthea',
+ fields=[
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this model has been created', verbose_name='Date created')),
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this model has been updated', verbose_name='Date updated')),
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='A unique id', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
+ ('state', models.CharField(help_text='The state for which synthea generate data.', max_length=200, verbose_name='Stage')),
+ ('population', models.PositiveSmallIntegerField(default=50, help_text='The size of the population', verbose_name='Population')),
+ ('gender', models.CharField(help_text='Select the gender type', max_length=1, verbose_name='Gender')),
+ ('age', models.CharField(help_text='Select the age range', max_length=10, verbose_name='Age range')),
+ ('module', models.CharField(help_text='Select the module', max_length=50, verbose_name='Mopdule')),
+ ],
+ options={
+ 'verbose_name': 'token',
+ 'verbose_name_plural': 'tokens',
+ },
+ ),
+ ]
diff --git a/webservice/apps/synthea/migrations/__init__.py b/webservice/apps/synthea/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/apps/synthea/models.py b/webservice/apps/synthea/models.py
new file mode 100644
index 0000000..8892f2d
--- /dev/null
+++ b/webservice/apps/synthea/models.py
@@ -0,0 +1,24 @@
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from django_cryptography.fields import encrypt
+
+from lib.utils.general import get_random_string
+from lib.models.base import MetaDataModel
+
+import uuid
+
+# Create your models here.
+class Synthea(MetaDataModel):
+
+ class Meta:
+ verbose_name = _('token')
+ verbose_name_plural = _('tokens')
+
+ 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'))
+ 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'))
+ module = models.CharField(_('Module'),blank=True, max_length=50, help_text=_('Select the module'))
diff --git a/webservice/apps/synthea/templates/synthea/generator_form.html b/webservice/apps/synthea/templates/synthea/generator_form.html
new file mode 100644
index 0000000..3aafae7
--- /dev/null
+++ b/webservice/apps/synthea/templates/synthea/generator_form.html
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "Generate a new Synthea data set" %}{% endblock %}
+{% block pagetitle %}{% trans "Generate a new Synthea data set" %}{% endblock %}
+
+{% block menu %}
+ {% include 'synthea/menu.html' %}
+{% endblock %}
+
+{% block content %}
+
Synthea generartor
+
Enter the form fields and press submit. U vraagt, wij draaien ;)
+
+{% endblock %}
diff --git a/webservice/apps/synthea/templates/synthea/index.html b/webservice/apps/synthea/templates/synthea/index.html
new file mode 100644
index 0000000..5e83ce1
--- /dev/null
+++ b/webservice/apps/synthea/templates/synthea/index.html
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "dHealth Synthea" %}{% endblock %}
+{% block pagetitle %}{% trans "dHealth Synthea" %}{% endblock %}
+
+{% block menu %}
+ {% include 'synthea/menu.html' %}
+{% endblock %}
+
+{% block content %}
+
\ No newline at end of file
diff --git a/webservice/apps/synthea/tests.py b/webservice/apps/synthea/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/webservice/apps/synthea/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/webservice/apps/synthea/urls.py b/webservice/apps/synthea/urls.py
new file mode 100644
index 0000000..e8ed444
--- /dev/null
+++ b/webservice/apps/synthea/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path, include
+
+from . import views
+
+urlpatterns = [
+ path('', views.index, name='index'),
+ path('api/', views.index, name='api_info'),
+ path('generate/', views.show_synthea_form, name='generator_form'),
+]
\ No newline at end of file
diff --git a/webservice/apps/synthea/views.py b/webservice/apps/synthea/views.py
new file mode 100644
index 0000000..a43e0bc
--- /dev/null
+++ b/webservice/apps/synthea/views.py
@@ -0,0 +1,49 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+from apps.synthea.forms import SyntheaForm
+
+from .lib.utils import run_synthea
+
+import mimetypes
+# Create your views here.
+
+def index(request):
+ template_name = 'synthea/index.html'
+ return render(request,template_name,{})
+
+def show_synthea_form(request):
+ template_name = 'synthea/generator_form.html'
+
+ # if this is a POST request we need to process the form data
+ if request.method == 'POST':
+ # create a form instance and populate it with data from the request:
+ form = SyntheaForm(request.POST)
+ # check whether it's valid:
+ if form.is_valid():
+ # process the data in form.cleaned_data as required
+
+ 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}'
+
+ return response
+
+ except Exception as ex:
+ print(ex)
+
+ # if a GET (or any other method) we'll create a blank form
+ else:
+ form = SyntheaForm()
+
+ return render(request,template_name,{
+ 'form':form
+ })
\ No newline at end of file
diff --git a/webservice/db.sqlite3 b/webservice/db.sqlite3
new file mode 100644
index 0000000..8e79434
Binary files /dev/null and b/webservice/db.sqlite3 differ
diff --git a/webservice/lib/api/base.py b/webservice/lib/api/base.py
new file mode 100644
index 0000000..755ad52
--- /dev/null
+++ b/webservice/lib/api/base.py
@@ -0,0 +1,45 @@
+from rest_framework import viewsets, permissions, serializers
+from rest_framework.permissions import BasePermission
+
+class IsOwner(BasePermission):
+ def has_object_permission (self, request, view, obj ):
+ """Return 'True' if permission is granted, 'False' otherwise."""
+ # TODO: If this is the 'way to go', we should consider adding the researcher reference to all models and save actions
+ return obj.researcher == request.user.researcher or obj.study.researcher == request.user.researcher
+
+class BaseReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
+ permission_classes = [permissions.IsAuthenticated, IsOwner]
+
+ # TODO: If this is the 'way to go', we should consider adding the researcher reference to all models and save actions
+ def get_queryset(self):
+ try:
+ qs = self.queryset.filter(researcher = self.request.user.researcher)
+ except:
+ qs = self.queryset.filter(study__researcher = self.request.user.researcher)
+
+ return qs
+
+class BaseViewSet(viewsets.ModelViewSet):
+ permission_classes = [permissions.IsAuthenticated, IsOwner]
+
+ # TODO: If this is the 'way to go', we should consider adding the researcher reference to all models and save actions
+ def get_queryset(self):
+ try:
+ qs = self.queryset.filter(researcher = self.request.user.researcher)
+ except:
+ qs = self.queryset.filter(study__researcher = self.request.user.researcher)
+
+ return qs
+
+class BaseHyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
+ # This ID field is handy to have.... Due to HyperlinkedModelSerializer we do not have this field by default
+ id = serializers.ReadOnlyField()
+
+ # Only show the researcher full name
+ researcher = serializers.StringRelatedField()
+
+ # Only show link to full researcher data
+ #researcher = serializers.HyperlinkedRelatedField(view_name= 'api:v1:researcher-detail', read_only=True)
+
+ # Show the full researcher information
+ #researcher = ResearcherSerializer(read_only=True)
diff --git a/webservice/lib/api/client.py b/webservice/lib/api/client.py
new file mode 100644
index 0000000..1049ce7
--- /dev/null
+++ b/webservice/lib/api/client.py
@@ -0,0 +1,145 @@
+import requests
+from requests_hawk import HawkAuth
+from datetime import datetime
+
+class VRE_API_Exception(BaseException):
+ def __init__(self, message, error):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message)
+ # Now for your custom code...
+ self.error = error
+
+ def __repr__(self):
+ return '{} ({})'.format(self.message, self.error)
+
+class VRE_API_Exception_Factory(VRE_API_Exception):
+
+ def __new__(self, message, error_code):
+ if error_code == 400:
+ return VRE_API_400(message)
+ elif error_code == 403:
+ return VRE_API_403(message)
+ elif error_code == 404:
+ return VRE_API_404(message)
+
+class VRE_API_400(VRE_API_Exception):
+ def __init__(self, message):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message, 403)
+
+class VRE_API_403(VRE_API_Exception):
+ def __init__(self, message):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message, 403)
+
+class VRE_API_404(VRE_API_Exception):
+ def __init__(self, message):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message, 404)
+
+class VRE_API_Client():
+
+ DATE_TIME_FIELDS = 'created_at,updated_at,mail_sent'.split(',')
+
+ HEADERS = {
+ 'Content-Type': 'application/json',
+ 'cache-control': 'no-cache'
+ }
+
+ def __init__(self, host, url = None, token = None, secret = None):
+ self.host = host
+ self.url = url
+ self.token = token
+ self.secret = secret
+
+ self.data = {}
+ self.authentication = HawkAuth(id=self.token , key=self.secret)
+
+ def __get_full_url(self):
+ return '{}{}'.format(self.host, self.url)
+
+ def __parse_date_time_fields(self, data):
+ for item in data:
+ # TODO: Should provide better solution for this try/catch. For now it works
+ try:
+ if isinstance(item,list) or isinstance(item,dict):
+ self.__parse_date_time_fields(item)
+
+ elif isinstance(data[item],list) or isinstance(data[item],dict):
+ self.__parse_date_time_fields(data[item])
+
+ elif item in self.DATE_TIME_FIELDS and isinstance(data[item],str):
+ try:
+ data[item] = datetime.strptime(data[item],'%Y-%m-%dT%H:%M:%S.%fZ')
+ except Exception:
+ data[item] = datetime.strptime(data[item][::-1].replace(':','',1)[::-1].replace(' ','T'),'%Y-%m-%dT%H:%M:%S.%f%z')
+ except Exception:
+ pass
+
+ def __parse_data(self, start = None):
+ if len(self.DATE_TIME_FIELDS) > 0:
+ self.__parse_date_time_fields(self.data)
+
+ def set_url(self, url):
+ self.url = url
+
+ def set_token(self, token):
+ self.token = token
+ self.authentication = HawkAuth(id=self.token , key=self.secret)
+
+ def set_secret(self, secret):
+ self.secret = secret
+ self.authentication = HawkAuth(id=self.token , key=self.secret)
+
+ def get_data(self):
+ result = requests.get(self.__get_full_url(), auth=self.authentication, headers=self.HEADERS)
+ self.data['status_code'] = result.status_code
+
+ if result.status_code in [200,201]:
+ self.data = result.json()
+ self.__parse_data()
+ else:
+ print(result.json())
+ raise VRE_API_Exception_Factory('Error with url {}.'.format(self.url),result.status_code)
+
+ return self.data
+
+ def post_data(self, payload):
+ result = requests.post(self.__get_full_url(), json=payload, auth=self.authentication, headers=self.HEADERS)
+ self.data['status_code'] = result.status_code
+
+ if result.status_code in [200,201]:
+ self.data = result.json()
+ self.__parse_data()
+ else:
+ #print(result.content)
+
+ #print(result.text)
+
+ raise VRE_API_Exception_Factory('Error with url {}.'.format(self.url),result.status_code)
+
+ return self.data
+
+ def put_data(self, payload):
+ result = requests.put(self.__get_full_url(), json=payload, auth=self.authentication, headers=self.HEADERS)
+ self.data['status_code'] = result.status_code
+
+ if result.status_code in [200,201]:
+ self.data = result.json()
+ self.__parse_data()
+ else:
+ print(result.json())
+ raise VRE_API_Exception_Factory('Error with url {}.'.format(self.url),result.status_code)
+
+ return self.data
+
+ def delete_data(self):
+ try:
+ # Django HAWK has issues with a delete action. It needs/wants a content-type header, but there is no content.....
+ # https://github.com/kumar303/hawkrest/issues/46
+ result = requests.delete(self.__get_full_url(), auth=self.authentication, headers=self.HEADERS)
+ return result.status_code in [200,201,204]
+ except Exception:
+ raise VRE_API_Exception_Factory('Error with url {}.'.format(self.url),result.status_code)
+
+ return False
\ No newline at end of file
diff --git a/webservice/lib/models/base.py b/webservice/lib/models/base.py
new file mode 100644
index 0000000..8a863c4
--- /dev/null
+++ b/webservice/lib/models/base.py
@@ -0,0 +1,20 @@
+
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+class MetaDataModel(models.Model):
+ """
+ This is an abstract Django model with some general meta fields that can be used for other models.
+
+ Attributes
+ ----------
+ created_at : datetime
+ The date and time when the model has been created. This will be automatically set once during creating.
+ updated_at : datetime
+ The date and time when the model has been updated. This will be automatically updated when the model is updated.
+ """
+ created_at = models.DateTimeField(_('Date created'),auto_now_add=True, help_text=_('The date and time this model has been created'))
+ updated_at = models.DateTimeField(_('Date updated'),auto_now=True, help_text=_('The date and time this model has been updated'))
+
+ class Meta:
+ abstract = True
\ No newline at end of file
diff --git a/webservice/lib/utils/emails.py b/webservice/lib/utils/emails.py
new file mode 100644
index 0000000..ac545e3
--- /dev/null
+++ b/webservice/lib/utils/emails.py
@@ -0,0 +1,84 @@
+import os.path
+import re
+import mimetypes
+
+from email.mime.base import MIMEBase
+from django.core.mail import EmailMultiAlternatives, SafeMIMEMultipart
+from django.conf import settings
+
+# Source: https://djangosnippets.org/snippets/2215/
+class EmailMultiRelated(EmailMultiAlternatives):
+ """
+ A version of EmailMessage that makes it easy to send multipart/related
+ messages. For example, including text and HTML versions with inline images.
+ """
+ related_subtype = 'related'
+
+ def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
+ connection=None, attachments=None, headers=None, alternatives=None):
+ # self.related_ids = []
+ self.related_attachments = []
+ super(EmailMultiRelated, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers, alternatives)
+
+ def attach_related(self, filename=None, content=None, mimetype=None):
+ """
+ Attaches a file with the given filename and content. The filename can
+ be omitted and the mimetype is guessed, if not provided.
+
+ If the first parameter is a MIMEBase subclass it is inserted directly
+ into the resulting message attachments.
+ """
+ if isinstance(filename, MIMEBase):
+ assert content == mimetype == None
+ self.related_attachments.append(filename)
+ else:
+ assert content is not None
+ self.related_attachments.append((filename, content, mimetype))
+
+ def attach_related_file(self, path, mimetype=None):
+ """Attaches a file from the filesystem."""
+ filename = os.path.basename(path)
+ content = open(path, 'rb').read()
+ if mimetype is None:
+ mimetypes.init()
+ mimetype = mimetypes.guess_type(filename)[0]
+ self.attach_related(filename, content, mimetype)
+
+ def _create_message(self, msg):
+ return self._create_attachments(self._create_related_attachments(self._create_alternatives(msg)))
+
+ def _create_alternatives(self, msg):
+ for i, (content, mimetype) in enumerate(self.alternatives):
+ if mimetype == 'text/html':
+ for filename, _, _ in self.related_attachments:
+ content = re.sub(r'(?' % filename)
+ return attachment
\ No newline at end of file
diff --git a/webservice/lib/utils/general.py b/webservice/lib/utils/general.py
new file mode 100644
index 0000000..05cce2c
--- /dev/null
+++ b/webservice/lib/utils/general.py
@@ -0,0 +1,26 @@
+import re
+import random
+import string
+
+def remove_html_tags(text):
+ """Remove html tags from a string"""
+ clean = re.compile('<.*?>')
+ return re.sub(clean, '', text)
+
+def get_random_int_value(length = 6):
+ return ''.join(list(map(lambda x: str(random.randint(1,9)), list(range(length)))))
+
+def get_random_string(length = 8):
+ return ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k=length))
+
+def generate_encryption_key(length = 32):
+ return get_random_string(length)
+
+def get_ip_address(request):
+ """ use requestobject to fetch client machine's IP Address """
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', None)
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0]
+ else:
+ ip = request.META.get('REMOTE_ADDR') ### Real IP address of client Machine
+ return ip
\ No newline at end of file
diff --git a/webservice/manage.py b/webservice/manage.py
new file mode 100755
index 0000000..61ddd5d
--- /dev/null
+++ b/webservice/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webservice.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/webservice/static/images/.gitignore b/webservice/static/images/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/static/javascript/javascript.js b/webservice/static/javascript/javascript.js
new file mode 100644
index 0000000..c3aadf9
--- /dev/null
+++ b/webservice/static/javascript/javascript.js
@@ -0,0 +1 @@
+/* Dummy javascript file */
\ No newline at end of file
diff --git a/webservice/static/style/style.css b/webservice/static/style/style.css
new file mode 100644
index 0000000..236c4b8
--- /dev/null
+++ b/webservice/static/style/style.css
@@ -0,0 +1 @@
+/* Dummy style sheet */
\ No newline at end of file
diff --git a/webservice/webservice/.env b/webservice/webservice/.env
new file mode 100644
index 0000000..c1d7308
--- /dev/null
+++ b/webservice/webservice/.env
@@ -0,0 +1,41 @@
+# A uniquely secret key
+SECRET_KEY=@wb=#(f4vc0l(e!5*eo+a@flnxb2@!l9!=c6w=4b+x$=!8&vy%'
+
+# Disable debug in production
+DEBUG=True
+
+# Allowed hosts that Django does server. Take care when NGINX is proxying infront of Django
+ALLOWED_HOSTS=127.0.0.1,localhost
+
+# All internal IPS for Django. Use comma separated list
+INTERNAL_IPS=127.0.0.1
+
+# Enter the database url connection: https://github.com/jacobian/dj-database-url
+DATABASE_URL=sqlite:////opt/development/synthea_webservice/webservice/db.sqlite3
+
+# The location on disk where the static files will be placed during deployment. Setting is required
+STATIC_ROOT=
+
+# Enter the default timezone for the visitors when it is not known.
+TIME_ZONE=Europe/Amsterdam
+
+# Email settings
+
+# Mail host
+EMAIL_HOST=192.168.5.1
+
+# Email user name
+EMAIL_HOST_USER=na
+
+# Email password
+EMAIL_HOST_PASSWORD=na
+
+# Email server port number to use
+EMAIL_PORT=25
+
+# Does the email server supports TLS?
+EMAIL_USE_TLS=yes
+
+# The sender address. This needs to be one of the allowed domains due to SPF checks
+# The code will use a reply-to header to make sure that replies goes to the researcher and not this address
+EMAIL_FROM_ADDRESS=Do not reply
\ No newline at end of file
diff --git a/webservice/webservice/.env.example b/webservice/webservice/.env.example
new file mode 100644
index 0000000..5c59f24
--- /dev/null
+++ b/webservice/webservice/.env.example
@@ -0,0 +1,53 @@
+# A uniquely secret key
+SECRET_KEY=@wb=#(f4uc0l%e!5*eo+aoflnxb(@!l9!=c5w=4b+x$=!8&vy%'
+
+# Disable debug in production
+DEBUG=False
+
+# Allowed hosts that Django does server. Take care when NGINX is proxying infront of Django
+ALLOWED_HOSTS=127.0.0.1,localhost
+
+# All internal IPS for Django. Use comma separated list
+INTERNAL_IPS=127.0.0.1
+
+# Enter the database url connection: https://github.com/jacobian/dj-database-url
+DATABASE_URL=sqlite:////opt/deploy/VRE/VirtualResearchEnvironment/db.sqlite3
+
+# The location on disk where the static files will be placed during deployment. Setting is required
+STATIC_ROOT=
+
+# Enter the default timezone for the visitors when it is not known.
+TIME_ZONE=Europe/Amsterdam
+
+# Email settings
+
+# Mail host
+EMAIL_HOST=
+
+# Email user name
+EMAIL_HOST_USER=
+
+# Email password
+EMAIL_HOST_PASSWORD=
+
+# Email server port number to use
+EMAIL_PORT=25
+
+# Does the email server supports TLS?
+EMAIL_USE_TLS=
+
+# The sender address. This needs to be one of the allowed domains due to SPF checks
+# The code will use a reply-to header to make sure that replies goes to the researcher and not this address
+EMAIL_FROM_ADDRESS=Do not reply
+
+# What is the Dropoff hostname (webinterface)
+DROPOFF_HOSTNAME=http://localhost:8000
+
+# What is the Dropoff Upload host
+DROPOFF_UPLOAD_HOST=http://localhost
+
+# Which file extensions are **NOT** allowed
+DROPOFF_NOT_ALLOWED_EXTENSIONS=exe,com,bat,lnk,sh
+
+# What is the full VRE Portal domains
+VRE_BROKER_API=http://localhost:8000
\ No newline at end of file
diff --git a/webservice/webservice/__init__.py b/webservice/webservice/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/webservice/asgi.py b/webservice/webservice/asgi.py
new file mode 100644
index 0000000..eb81156
--- /dev/null
+++ b/webservice/webservice/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for webservice project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webservice.settings')
+
+application = get_asgi_application()
diff --git a/webservice/webservice/settings.py b/webservice/webservice/settings.py
new file mode 100644
index 0000000..2aee057
--- /dev/null
+++ b/webservice/webservice/settings.py
@@ -0,0 +1,166 @@
+"""
+Django settings for webservice project.
+
+Generated by 'django-admin startproject' using Django 3.1.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+from decouple import config, Csv
+from django.utils.translation import ugettext_lazy as _
+from dj_database_url import parse as db_url
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = config('SECRET_KEY')
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = config('DEBUG', default=False, cast=bool)
+
+ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ 'django_js_reverse',
+
+ 'apps.api',
+ 'apps.RUG_template',
+ 'apps.synthea',
+
+ 'django.contrib.admin', # Admin add here, so that we can overrule the password reset template
+]
+
+if DEBUG:
+ INSTALLED_APPS.append('debug_toolbar')
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+if DEBUG:
+ MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
+
+INTERNAL_IPS = config('INTERNAL_IPS',default='127.0.0.1',cast=Csv())
+
+ROOT_URLCONF = 'webservice.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'webservice.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': config(
+ 'DATABASE_URL',
+ default='sqlite:///' + str(BASE_DIR / 'db.sqlite3'),
+ cast=db_url
+ )
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+# SSL Checks / Setup
+# This will tell Django if the request is trough SSL (proxy). This is needed for Hawk authentication
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+LANGUAGES = [
+ ('nl', _('Dutch')),
+ ('en', _('English')),
+]
+
+TIME_ZONE = config('TIME_ZONE', default='UTC')
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
+
+STATICFILES_DIRS = [
+ BASE_DIR / 'static',
+]
+
+STATIC_ROOT = config('STATIC_ROOT',None)
+
+EMAIL_FROM_ADDRESS = config('EMAIL_FROM_ADDRESS', default='Do not reply')
+EMAIL_HOST = config('EMAIL_HOST', default='')
+EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
+EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
+EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
+EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
+
+DEFAULT_FROM_EMAIL = EMAIL_FROM_ADDRESS
+
+if DEBUG:
+ EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
+ EMAIL_FILE_PATH = BASE_DIR / 'sent_emails'
\ No newline at end of file
diff --git a/webservice/webservice/urls.py b/webservice/webservice/urls.py
new file mode 100644
index 0000000..ba9474e
--- /dev/null
+++ b/webservice/webservice/urls.py
@@ -0,0 +1,36 @@
+"""webservice URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.contrib import admin
+from django.urls import path, include
+from django_js_reverse.views import urls_js
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ path('', include('apps.synthea.urls')),
+
+ # Add Default RUG HTML homepage
+ path('', include('apps.RUG_template.urls')),
+ path('jsreverse/', urls_js, name='js_reverse'),
+ path('i18n/', include('django.conf.urls.i18n')),
+]
+
+if settings.DEBUG:
+ import debug_toolbar
+ urlpatterns = [
+ path('__debug__/', include(debug_toolbar.urls)),
+ ] + urlpatterns
\ No newline at end of file
diff --git a/webservice/webservice/wsgi.py b/webservice/webservice/wsgi.py
new file mode 100644
index 0000000..5451acf
--- /dev/null
+++ b/webservice/webservice/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for webservice project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webservice.settings')
+
+application = get_wsgi_application()