diff --git a/Vagrantfile b/Vagrantfile index a10af759..3a805c60 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -18,7 +18,8 @@ Vagrant.configure("2") do |config| apt-get clean rm -rf /var/lib/apt/lists/* apt-get update - apt-get install -y unzip apache2 python3 python3-pip mysql-server libmysqlclient-dev python3-dev libssl-dev python3-sphinx libpq-dev virtualenv + apt-get install -y unzip apache2 python3 python3-pip mysql-server libmysqlclient-dev \ + python3-dev libssl-dev python3-sphinx libpq-dev virtualenv apt-get upgrade -y pip3 install -r /home/vagrant/femr_onchain/requirements.txt curl -sL https://deb.nodesource.com/setup_14.x | bash - diff --git a/appMR b/appMR index c6534217..943eae40 160000 --- a/appMR +++ b/appMR @@ -1 +1 @@ -Subproject commit c65342175887e8f8682867b5037c9b8eb614cb02 +Subproject commit 943eae40f894e159f0bd66404fdaebd87c729428 diff --git a/femr_onchain/settings.py b/femr_onchain/settings.py index c432df6f..dbbae5ba 100644 --- a/femr_onchain/settings.py +++ b/femr_onchain/settings.py @@ -50,7 +50,7 @@ 'session_security', 'django_user_agents', 'background_task', - 'rest_framework_swagger', + 'drf_yasg', ] AUTHENTICATION_BACKENDS = [ diff --git a/femr_onchain/urls.py b/femr_onchain/urls.py index b11b72e4..9e674005 100644 --- a/femr_onchain/urls.py +++ b/femr_onchain/urls.py @@ -1,3 +1,6 @@ +""" +Main URL configurations for fEMR-OnChain-Core. This redirects to the other apps in this project. +""" from django.conf.urls import url from django.contrib import admin from django.contrib.auth import views as auth_views diff --git a/main/admin.py b/main/admin.py index 08e6ba84..c74fa3c9 100644 --- a/main/admin.py +++ b/main/admin.py @@ -11,10 +11,16 @@ class TreatmentAdmin(admin.ModelAdmin): + """ + Register a TreatmentForm as accessible in the Admin site. + """ form = TreatmentForm class PatientEncounterAdmin(admin.ModelAdmin): + """ + Register a PatientEncounterForm as accessible in the Admin site. + """ form = PatientEncounterForm diff --git a/main/background_tasks.py b/main/background_tasks.py index e8f77ff4..9927f3b2 100644 --- a/main/background_tasks.py +++ b/main/background_tasks.py @@ -1,3 +1,6 @@ +""" +Non-view functions used to carry out background processes. +""" from datetime import timedelta from django.utils import timezone @@ -26,7 +29,11 @@ def run_encounter_close(): e.save_no_timestamp() -def reset_sessions(): +def reset_sessions() -> None: + """ + Empty out sessions older than 1 minute. + :return: + """ now = timezone.now() d = now - timedelta(minutes=1) for x in UserSession.objects.all(): diff --git a/main/csvio/added_inventory.py b/main/csvio/added_inventory.py index c109aa39..fecfa695 100644 --- a/main/csvio/added_inventory.py +++ b/main/csvio/added_inventory.py @@ -1,3 +1,6 @@ +""" +AddedInventoryHandler and required imports. +""" import csv import requests @@ -7,13 +10,28 @@ class AddedInventoryHandler(CSVHandler): + """ + Implements CSVHandler and adds inventory to supplies that already existed in the formulary. + """ def __init__(self) -> None: super().__init__() def read(self, upload, campaign): + """ + Given an upload file and a campaign to upload to, execute a CSV import. + :param upload: + :param campaign: + :return: + """ return self.__import(upload, campaign) def write(self, response, formulary): + """ + Given an HTTPResponse and a formulary object, write out the formulary as a CSV file. + :param response: + :param formulary: + :return: + """ return self.__export(response, formulary) def __export(self, response, formulary): diff --git a/main/delete_views.py b/main/delete_views.py index bbc64c63..df708af7 100644 --- a/main/delete_views.py +++ b/main/delete_views.py @@ -60,6 +60,14 @@ def patient_delete_view(request, id=None): def delete_chief_complaint(request, id=None, patient_id=None, encounter_id=None): + """ + Delete a selected Chief Complaint and redirect back. + :param request: + :param id: + :param patient_id: + :param encounter_id: + :return: + """ if request.user.is_authenticated: p = get_object_or_404(ChiefComplaint, pk=id) p.active = False diff --git a/main/edit_views.py b/main/edit_views.py index b4c59960..64b67b9b 100644 --- a/main/edit_views.py +++ b/main/edit_views.py @@ -602,6 +602,7 @@ def upload_photo_view(request, patient_id=None, encounter_id=None): ph.save() m.photos.add(ph) m.save() + aux_form = PhotoForm() DatabaseChangeLog.objects.create(action="Edit", model="PatientEncounter", instance=str(m), ip=get_client_ip(request), username=request.user.username, campaign=Campaign.objects.get(name=request.session['campaign'])) diff --git a/main/form_views.py b/main/form_views.py index 5e9b55af..98ae35be 100644 --- a/main/form_views.py +++ b/main/form_views.py @@ -78,7 +78,7 @@ def patient_form_view(request): 'match_list': match, 'form': form, 'page_name': 'New Patient', - 'page_tip': "Complete form with patient demographics as instructed. Any box with an asterix (*) is required. Shared contact information would be if two patients have a household phone or email that they share, for example."}) + 'page_tip': "Complete form with patient demographics as instructed. Any box with an asterisk (*) is required. Shared contact information would be if two patients have a household phone or email that they share, for example."}) else: return redirect('/not_logged_in') @@ -194,7 +194,7 @@ def patient_encounter_form_view(request, id=None): 'page_name': 'New Encounter for {} {} {}'.format(p.first_name, p.last_name, suffix), 'birth_sex': p.sex_assigned_at_birth, 'patient_id': id, 'units': units, 'telehealth': telehealth, 'encounter_open': encounter_open, - 'page_tip': "Complete form with patient vitals as instructed. Any box with an asterix (*) is required. For max efficiency, use 'tab' to navigate through this page."}) + 'page_tip': "Complete form with patient vitals as instructed. Any box with an asterisk (*) is required. For max efficiency, use 'tab' to navigate through this page."}) else: return redirect('/not_logged_in') diff --git a/main/forms.py b/main/forms.py index ec32f54a..b0c5a6d7 100644 --- a/main/forms.py +++ b/main/forms.py @@ -304,7 +304,7 @@ def save(self, commit=True): m.body_height_secondary = ( ((tmp * 12) + m.body_height_secondary) * 2.54) % 100 if m.body_weight is not None: - m.body_weight = m.body_weight / 2.2046 + m.body_weight /= 2.2046 if commit: m.save() return m diff --git a/main/formulary_management.py b/main/formulary_management.py index 74c81612..6fe20749 100644 --- a/main/formulary_management.py +++ b/main/formulary_management.py @@ -1,4 +1,3 @@ -from main.csvio.added_inventory import AddedInventoryHandler from django.db.models.query_utils import Q from django.http.response import HttpResponse from django.shortcuts import redirect, render @@ -55,9 +54,8 @@ def edit_add_supply_view(request, id=None): elif request.method == "POST": form = AddSupplyForm(request.POST) if form.is_valid(): - inventory_entry.initial_quantity = inventory_entry.initial_quantity + \ - int(request.POST['quantity']) - inventory_entry.quantity = inventory_entry.quantity + int(request.POST['quantity']) + inventory_entry.initial_quantity += int(request.POST['quantity']) + inventory_entry.quantity += int(request.POST['quantity']) inventory_entry.save() return redirect('main:formulary_home_view') else: @@ -84,9 +82,8 @@ def edit_sub_supply_view(request, id=None): if form.is_valid(): if inventory_entry.initial_quantity > int( request.POST['quantity']) and inventory_entry.quantity > int(request.POST['quantity']): - inventory_entry.initial_quantity = inventory_entry.initial_quantity - \ - int(request.POST['quantity']) - inventory_entry.quantity = inventory_entry.quantity - int(request.POST['quantity']) + inventory_entry.initial_quantity -= int(request.POST['quantity']) + inventory_entry.quantity -= int(request.POST['quantity']) else: inventory_entry.initial_quantity = 0 inventory_entry.quantity = 0 diff --git a/main/list_views.py b/main/list_views.py index 23b7503c..a1eb1af0 100644 --- a/main/list_views.py +++ b/main/list_views.py @@ -70,7 +70,7 @@ def patient_csv_export_view(request): 'Body Temperature (F)', 'Height', 'Weight (lbs)', 'BMI', 'Oxygen Concentration', 'Glucose Level', 'History of Tobacco Use', 'History of Diabetes', 'History of Hypertension', 'History of High Cholesterol', - 'History of Alchol Abuse/Substance Abuse', 'Community Health Worker Notes', + 'History of Alcohol Abuse/Substance Abuse', 'Community Health Worker Notes', 'Procedure/Counseling', 'Pharmacy Notes', 'Medical/Surgical History', 'Social History', 'Current Medications', 'Family History'] else: @@ -80,9 +80,9 @@ def patient_csv_export_view(request): 'Glucose Level', 'History of Tobacco Use', 'History of Diabetes', 'History of Hypertension', 'History of High Cholesterol', - 'History of Alchol Abuse/Substance Abuse', 'Community Health Worker Notes', + 'History of Alcohol Abuse/Substance Abuse', 'Community Health Worker Notes', 'Procedure/Counseling', 'Pharmacy Notes', - 'Medical/Surgical History', 'Socil History', 'Current Medications', 'Family History'] + 'Medical/Surgical History', 'Social History', 'Current Medications', 'Family History'] try: data = Patient.objects.filter( campaign=Campaign.objects.get(name=request.session['campaign'])).exclude( diff --git a/main/middleware.py b/main/middleware.py index dc1eb1a2..3aa52bac 100644 --- a/main/middleware.py +++ b/main/middleware.py @@ -1,8 +1,7 @@ import pytz from django.contrib.auth import logout from django.contrib.auth.models import AnonymousUser -from django.contrib.sessions.exceptions import SessionInterrupted -from django.shortcuts import redirect, render +from django.shortcuts import render from django.utils import timezone from clinic_messages.models import Message diff --git a/main/static/main/js/bmi_calc.js b/main/static/main/js/bmi_calc.js index d6d4f6ec..0d606469 100644 --- a/main/static/main/js/bmi_calc.js +++ b/main/static/main/js/bmi_calc.js @@ -11,7 +11,7 @@ function get_input_and_calc() { } function set_final_value() { - console.log("Imperial BMI compuation."); + console.log("Imperial BMI computation."); $("#id_form-body_mass_index").val(get_input_and_calc); } diff --git a/main/static/main/js/bmi_calc_metric.js b/main/static/main/js/bmi_calc_metric.js index 476da569..f4a6e6b1 100644 --- a/main/static/main/js/bmi_calc_metric.js +++ b/main/static/main/js/bmi_calc_metric.js @@ -11,7 +11,7 @@ function get_input_and_calc() { } function set_final_value() { - console.log("Metric BMI compuation."); + console.log("Metric BMI computation."); $("#id_form-body_mass_index").val(get_input_and_calc); } diff --git a/main/static/main/js/bmi_edit_calc.js b/main/static/main/js/bmi_edit_calc.js index 54473622..0bda76fd 100644 --- a/main/static/main/js/bmi_edit_calc.js +++ b/main/static/main/js/bmi_edit_calc.js @@ -11,7 +11,7 @@ function get_input_and_calc() { } function set_final_value() { - console.log("Imperial BMI compuation."); + console.log("Imperial BMI computation."); $("#id_body_mass_index").val(get_input_and_calc); } diff --git a/main/static/main/js/bmi_edit_calc_metric.js b/main/static/main/js/bmi_edit_calc_metric.js index 476da569..f4a6e6b1 100644 --- a/main/static/main/js/bmi_edit_calc_metric.js +++ b/main/static/main/js/bmi_edit_calc_metric.js @@ -11,7 +11,7 @@ function get_input_and_calc() { } function set_final_value() { - console.log("Metric BMI compuation."); + console.log("Metric BMI computation."); $("#id_form-body_mass_index").val(get_input_and_calc); } diff --git a/main/templates/admin/home.html b/main/templates/admin/home.html index cb1afdba..5e1a1e6a 100644 --- a/main/templates/admin/home.html +++ b/main/templates/admin/home.html @@ -6,8 +6,8 @@

Campaign Management

User Management -
Access Audit Log Management -
Database Access Log Management -
diff --git a/main/templates/data/base.html b/main/templates/data/base.html index 52962395..5456e131 100644 --- a/main/templates/data/base.html +++ b/main/templates/data/base.html @@ -8,8 +8,6 @@ fEMR On-Chain - @@ -35,7 +33,10 @@ - @@ -63,8 +64,8 @@ {% if request.message_number > 0 %} {% else %}
- v1.4.2.10132021 + v1.4.3.10142021
fEMR On-Chain Wiki diff --git a/main/templates/data/base_login.html b/main/templates/data/base_login.html index 8ce7fa15..51d2f622 100644 --- a/main/templates/data/base_login.html +++ b/main/templates/data/base_login.html @@ -50,7 +50,7 @@
- v1.4.2.10132021 + v1.4.3.10142021
diff --git a/main/templates/data/home.html b/main/templates/data/home.html index 3b364b02..ea1bb4eb 100644 --- a/main/templates/data/home.html +++ b/main/templates/data/home.html @@ -9,7 +9,7 @@
{% endif %}
- Welcome to fEMR On-Chain v1.4.2.10132021, {{ user.first_name }} {{ user.last_name }}! + Welcome to fEMR On-Chain v1.4.3.10142021, {{ user.first_name }} {{ user.last_name }}!
Please select a tab at the top, search below, or select a campaign below to get started! diff --git a/main/templates/data/patient_deleted_success.html b/main/templates/data/patient_deleted_success.html index ca67b1f6..5e82fac6 100644 --- a/main/templates/data/patient_deleted_success.html +++ b/main/templates/data/patient_deleted_success.html @@ -1,6 +1,6 @@ {% extends "data/base.html" %} {% block content %}
-
Paitent record successfully deleted.
+
Patient record successfully deleted.
{% endblock %} diff --git a/main/templates/forms/edit_encounter.html b/main/templates/forms/edit_encounter.html index 466b20c2..0cbd4f58 100644 --- a/main/templates/forms/edit_encounter.html +++ b/main/templates/forms/edit_encounter.html @@ -301,7 +301,8 @@

Treatments

{{ form.chief_complaint|as_crispy_field }} - Manage
diff --git a/main/templates/forms/encounter.html b/main/templates/forms/encounter.html index 54dd3645..1be9a016 100644 --- a/main/templates/forms/encounter.html +++ b/main/templates/forms/encounter.html @@ -170,8 +170,9 @@
-
diff --git a/main/templates/list/patient.html b/main/templates/list/patient.html index ba33817d..6d7c6666 100644 --- a/main/templates/list/patient.html +++ b/main/templates/list/patient.html @@ -76,7 +76,8 @@ {{ o.campaign_key }} {{ o|open_encounters }} - {{ o.first_name }} {{ o|has_middle_name }} {{ o.last_name }} {{ o|has_suffix }} + {{ o.first_name }} + {{ o|has_middle_name }} {{ o.last_name }} {{ o|has_suffix }} {{ o|last_timestamp }} {{ o|get_campaign_info }} Medical diff --git a/main/templatetags/campaign_tags.py b/main/templatetags/campaign_tags.py index d1a23233..ef1f8bff 100644 --- a/main/templatetags/campaign_tags.py +++ b/main/templatetags/campaign_tags.py @@ -1,8 +1,19 @@ +""" +TemplateTags used to carry out more complex display actions on Campaigns. +""" from django import template +from main.models import Campaign + register = template.Library() @register.filter('is_selected') -def is_selected(campaign, selected): +def is_selected(campaign: Campaign, selected: str) -> bool: + """ + Given a campaign and a campaign name, check if they match. + :param campaign: + :param selected: + :return: + """ return True if campaign.name == selected else False diff --git a/main/templatetags/user_tags.py b/main/templatetags/user_tags.py index 970b630a..398f3fe2 100644 --- a/main/templatetags/user_tags.py +++ b/main/templatetags/user_tags.py @@ -1,10 +1,13 @@ +""" +Django TemplateTags for processing complex logic interacting with fEMRUser objects in templates. +""" # -*- coding:utf-8 -*- from __future__ import unicode_literals # Core Django imports from django import template -from main.models import Campaign +from main.models import Campaign, fEMRUser # Stdlib imports @@ -17,18 +20,35 @@ @register.filter('has_group') -def has_group(user, group_name): +def has_group(user: fEMRUser, group_name: str) -> bool: + """ + Given group_name, return whether user is a member of that group. + :param user: + :param group_name: + :return: + """ groups = user.groups.all().values_list('name', flat=True) return True if group_name in groups else False @register.filter('has_campaign') -def has_campaign(user, campaign_name): +def has_campaign(user: fEMRUser, campaign_name: str) -> bool: + """ + Given a campaign name, return whether user is a member of that campaign. + :param user: + :param campaign_name: + :return: + """ campaign = Campaign.objects.get(name=campaign_name) campaign_list = user.campaigns.all() return True if campaign in campaign_list else False @register.filter('campaign_active') -def campaign_active(campaign_name): +def campaign_active(campaign_name: str) -> bool: + """ + Return whether the given campaign_name refers to an active campaign. + :param campaign_name: + :return: + """ return Campaign.objects.get(name=campaign_name).active diff --git a/main/urls.py b/main/urls.py index 71ed970d..12b64d8f 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,8 +1,14 @@ +""" +URL configurations for the fEMR OnChain module. +""" from django.conf.urls import url, include from django.urls import path from rest_framework import routers from rest_framework.authtoken import views as rest_framework_views -from rest_framework_swagger.views import get_swagger_view + +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi from main.admin_views import add_user_to_campaign, add_users_to_campaign, admin_home, create_user_view, \ cut_user_from_campaign, export_audit_logs_view, export_database_logs_view, \ @@ -27,17 +33,30 @@ new_treatment_view, patient_edit_form_view, encounter_edit_form_view, patient_export_view, patient_medical, \ new_vitals_view, submit_hpi_view, upload_photo_view from .femr_admin_views import edit_contact_view, edit_organization_view, list_organization_view, lock_campaign_view, \ - new_campaign_view, new_contact_view, new_ethnicity_view, new_instance_view, edit_campaign_view, edit_instance_view, \ + new_campaign_view, new_contact_view, new_ethnicity_view, new_instance_view, edit_campaign_view, \ list_campaign_view, list_instance_view, femr_admin_home, change_campaign, new_organization_view, new_race_view, \ - unlock_campaign_view, view_contact_view + unlock_campaign_view, view_contact_view, edit_instance_view from .form_views import patient_form_view, referral_form_view, patient_encounter_form_view -from .list_views import chief_complaint_list_view, patient_csv_export_view, patient_list_view, search_patient_list_view, \ - filter_patient_list_view +from .list_views import chief_complaint_list_view, patient_csv_export_view, patient_list_view, \ + filter_patient_list_view, search_patient_list_view from .small_forms_views import chief_complaint_form_view, diagnosis_form_view, medication_form_view, treatment_form_view from .views import forgot_username, index, home, healthcheck, help_messages_off app_name = 'main' +schema_view = get_schema_view( + openapi.Info( + title="fEMR OnChain API", + default_version='v1', + description="API endpoints providing an interface with the fEMR OnChain application.", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="info@teamfemr.org"), + license=openapi.License(name="GPL v3"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + router = routers.DefaultRouter() router.register(r'Users', UserViewSet) router.register(r'Groups', GroupViewSet) @@ -221,7 +240,9 @@ path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^get_auth_token/$', rest_framework_views.obtain_auth_token, name='get_auth_token'), - path('swagger/', get_swagger_view(title='fEMR OnChain API')), + url(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), url(r'^forgot_username', forgot_username, name='forgot_username'), diff --git a/main/views.py b/main/views.py index 5b0db8e2..7b165ce9 100644 --- a/main/views.py +++ b/main/views.py @@ -10,7 +10,7 @@ from django.utils import timezone from main.forms import ForgotUsernameForm -from main.models import Campaign, MessageOfTheDay, fEMRUser +from main.models import Campaign, MessageOfTheDay, Photo, fEMRUser # noinspection PyUnusedLocal @@ -31,6 +31,9 @@ def home(request): :param request: Django Request object. :return: An HttpResponse, rendering the home page. """ + for x in Photo.objects.all(): + if not x.photo.name: + x.delete() if request.user.is_authenticated: motd = MessageOfTheDay.load() if motd.start_date is not None or motd.end_date is not None: @@ -96,6 +99,7 @@ def forgot_username(request): try: user = fEMRUser.objects.get(email__iexact=request.POST['email']) from django.core.mail import send_mail + # noinspection LongLine send_mail( 'Username Recovery', 'Someone recently requested a username reminder from fEMR On-Chain. If this was you, your username is:\n\n\n {}\n\n\n If it wasn\'t you, you can safely ignore this email.\n\n\nTHIS IS AN AUTOMATED MESSAGE FROM fEMR ON-CHAIN. PLEASE DO NOT REPLY TO THIS EMAIL. PLEASE LOG IN TO fEMR ON-CHAIN TO REPLY.'.format( diff --git a/requirements.txt b/requirements.txt index 6fff7c71..fec57265 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ djangorestframework==3.12.4 docutils==0.17.1 dparse==0.5.1 idna==3.3 +drf-yasg==1.20.0 ionhash==1.2.1 isort==5.9.3 jmespath==0.10.0