From c56bd8d245fca89237e8b6c8dc7929642c599fdf Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 7 Feb 2024 09:34:24 -0700 Subject: [PATCH 1/4] PPM-766: Updated for FHIR R4 and other associated changes --- app/api/urls.py | 2 +- app/api/views.py | 28 +- app/consent/fhir/example-signature.json | 27 + .../fhir/guardian-signature-part-1.json | 27 + .../fhir/guardian-signature-part-2.json | 34 +- .../fhir/guardian-signature-part-3.json | 27 + .../fhir/individual-signature-part-1.json | 27 + app/consent/fhir/neer-signature-v2.json | 27 + app/consent/fhir/neer-signature-v3.json | 27 + app/consent/fhir/neer-signature.json | 27 + .../fhir/ppm-asd-consent-guardian-quiz.json | 43 +- .../fhir/ppm-asd-consent-individual-quiz.json | 43 +- app/consent/fhir/rant-signature.json | 27 + app/consent/forms.py | 2 +- .../management/commands/updatefhirconsents.py | 8 +- .../templates/consent/example/_consent.html | 2 +- .../templates/consent/neer/_consent.html | 2 +- app/consent/urls.py | 2 +- app/fhirquestionnaire/fhir.py | 783 +++--------------- app/fhirquestionnaire/settings.py | 2 - app/questionnaire/fhir/asd.json | 76 +- app/questionnaire/fhir/neer.json | 39 +- app/questionnaire/forms.py | 52 +- .../commands/updatefhirquestionnaires.py | 8 +- 24 files changed, 557 insertions(+), 785 deletions(-) diff --git a/app/api/urls.py b/app/api/urls.py index 91a1302..bde8481 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -9,7 +9,7 @@ # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ - re_path(r'^consent/(?P[\w\d-]+)/(?P[\d]+)/?$', views.ConsentView.as_view(), name='consent'), + re_path(r'^consent/(?P[\w\d-]+)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[\d]+)/?$', views.ConsentView.as_view(), name='consent'), re_path(r'^consent/(?P[\w\d-]+)/?$', views.ConsentsView.as_view(), name='consents'), re_path(r'^questionnaire/?$', views.QuestionnaireView.as_view(), name='questionnaire'), re_path(r'^questionnaire/(?P[\w\d-]+)/?$', views.QuestionnaireView.as_view(), name='questionnaire'), diff --git a/app/api/views.py b/app/api/views.py index 5ba7e8f..e26f819 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -49,7 +49,7 @@ def get(self, request, study, ppm_id, format=None): try: # Check patient - if not FHIR.query_patient(patient=ppm_id): + if not FHIR.get_patient(patient=ppm_id): return Response(f'Participant {ppm_id} does not exist', status=status.HTTP_404_NOT_FOUND) # Check FHIR @@ -82,7 +82,7 @@ def post(self, request, study, ppm_id=None, format=None): try: # Check patient - if not FHIR.query_patient(patient=ppm_id): + if not FHIR.get_patient(patient=ppm_id): return Response(f'Participant {ppm_id} does not exist', status=status.HTTP_404_NOT_FOUND) # Check if we should overwrite @@ -140,18 +140,8 @@ def delete_consent_document_reference(request, study, ppm_id, document_reference # Get their consent composition composition = FHIR.get_consent_composition(patient=ppm_id, study=study) if not composition: - compositions = FHIR.query_consent_compositions(patient=ppm_id) - if not compositions: - logger.error(f'PPM/{study}/{ppm_id}: No consent compositions') - return False - - elif len(compositions) > 1: - logger.error(f'PPM/{study}/{ppm_id}: Too many generic consent ' - f'compositions: {[r["resource"]["id"] for r in compositions]}') - return False - - else: - composition = compositions[0] + logger.error(f'PPM/{study}/{ppm_id}: No consent compositions') + return False # Update it if FHIR.update_consent_composition(patient=ppm_id, study=study, composition=composition): @@ -189,7 +179,7 @@ def create_consent_document_reference(request, study, ppm_id=None): ppm_id = FHIR.query_patient_id(get_jwt_email(request=request, verify=False)) # Pull their record - bundle = FHIR.query_participant(patient=ppm_id, flatten_return=True) + participant = FHIR.get_participant(patient=ppm_id, flatten_return=True) # Get study title study_title = PPM.Study.title(study) @@ -204,7 +194,7 @@ def create_consent_document_reference(request, study, ppm_id=None): # Submit consent PDF logger.debug(f"PPM/{study}: Rendering consent with template: {template_name}") response = render_pdf(f'People-Powered Medicine {study_title} Consent', request, template_name, - context=bundle.get('composition'), options={}) + context=participant.get('composition'), options={}) # Hash the content hash = hashlib.md5(response.content).hexdigest() @@ -409,9 +399,9 @@ def get(self, request, questionnaire_id=None, format=None): if questionnaire_id: # Try to fetch it - questionnaire = FHIR.get_questionnaire( - questionnaire_id=questionnaire_id, - flatten_return=False + questionnaire = FHIR.fhir_read( + resource_type="Questionnaire", + resource_id=questionnaire_id, ) # Return it or 404 diff --git a/app/consent/fhir/example-signature.json b/app/consent/fhir/example-signature.json index 3484bf5..8f32de0 100644 --- a/app/consent/fhir/example-signature.json +++ b/app/consent/fhir/example-signature.json @@ -6,6 +6,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-example"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-example", + "display": "ppm-example" + } + ], + "text": "ppm-example" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/guardian-signature-part-1.json b/app/consent/fhir/guardian-signature-part-1.json index 9cfaa6c..62cbc4f 100644 --- a/app/consent/fhir/guardian-signature-part-1.json +++ b/app/consent/fhir/guardian-signature-part-1.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/guardian-signature-part-2.json b/app/consent/fhir/guardian-signature-part-2.json index 0862e79..04640fa 100644 --- a/app/consent/fhir/guardian-signature-part-2.json +++ b/app/consent/fhir/guardian-signature-part-2.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "display-1", @@ -23,9 +50,9 @@ { "linkId": "question-1", "text": "I acknowledge that I have explained this study to my child or individual in my care who will be participating.", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "yes" }, @@ -41,7 +68,8 @@ "enableWhen": [ { "question": "question-1", - "answerString": "no" + "answerString": "no", + "operator": "=" } ], "required": true diff --git a/app/consent/fhir/guardian-signature-part-3.json b/app/consent/fhir/guardian-signature-part-3.json index d454020..ba1072c 100644 --- a/app/consent/fhir/guardian-signature-part-3.json +++ b/app/consent/fhir/guardian-signature-part-3.json @@ -6,6 +6,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/individual-signature-part-1.json b/app/consent/fhir/individual-signature-part-1.json index 71f8746..3389130 100644 --- a/app/consent/fhir/individual-signature-part-1.json +++ b/app/consent/fhir/individual-signature-part-1.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/neer-signature-v2.json b/app/consent/fhir/neer-signature-v2.json index 84c83a6..0a2a72c 100644 --- a/app/consent/fhir/neer-signature-v2.json +++ b/app/consent/fhir/neer-signature-v2.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-neer"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-neer", + "display": "ppm-neer" + } + ], + "text": "ppm-neer" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/neer-signature-v3.json b/app/consent/fhir/neer-signature-v3.json index 486b4e9..6578519 100644 --- a/app/consent/fhir/neer-signature-v3.json +++ b/app/consent/fhir/neer-signature-v3.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-neer"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-neer", + "display": "ppm-neer" + } + ], + "text": "ppm-neer" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/neer-signature.json b/app/consent/fhir/neer-signature.json index e2c0c2d..79c8276 100644 --- a/app/consent/fhir/neer-signature.json +++ b/app/consent/fhir/neer-signature.json @@ -14,6 +14,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-neer"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-neer", + "display": "ppm-neer" + } + ], + "text": "ppm-neer" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/fhir/ppm-asd-consent-guardian-quiz.json b/app/consent/fhir/ppm-asd-consent-guardian-quiz.json index 2e36c03..cd4a36c 100644 --- a/app/consent/fhir/ppm-asd-consent-guardian-quiz.json +++ b/app/consent/fhir/ppm-asd-consent-guardian-quiz.json @@ -6,13 +6,40 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "question-1", "text": "In this consent, which is TRUE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "You agreed that you could be contacted in the future." }, @@ -27,9 +54,9 @@ { "linkId": "question-2", "text": "Which of the following is TRUE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "This study will store your child's social web data (e.g., Facebook, Twitter) with your permission, along with your child's health record data." }, @@ -44,9 +71,9 @@ { "linkId": "question-3", "text": "Which of the following is FALSE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "We will use your child's donated saliva sample for DNA (genetic code) sequencing." }, @@ -61,9 +88,9 @@ { "linkId": "question-4", "text": "Which of the following is FALSE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "You can download all stored data you provide us with and send it wherever you wish." }, diff --git a/app/consent/fhir/ppm-asd-consent-individual-quiz.json b/app/consent/fhir/ppm-asd-consent-individual-quiz.json index e3a5530..abe4261 100644 --- a/app/consent/fhir/ppm-asd-consent-individual-quiz.json +++ b/app/consent/fhir/ppm-asd-consent-individual-quiz.json @@ -14,13 +14,40 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "item": [ { "linkId": "question-1", "text": "In this consent, which is TRUE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "You agreed that you could be contacted in the future." }, @@ -35,9 +62,9 @@ { "linkId": "question-2", "text": "Which of the following is TRUE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "This study will store your social web data (e.g., Facebook, Twitter) with your permission, along with your health record data." }, @@ -52,9 +79,9 @@ { "linkId": "question-3", "text": "Which of the following is FALSE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "We will use your donated saliva sample for DNA (genetic code) sequencing." }, @@ -69,9 +96,9 @@ { "linkId": "question-4", "text": "Which of the following is FALSE?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "You can download all data you give us that we store and send it wherever you wish." }, diff --git a/app/consent/fhir/rant-signature.json b/app/consent/fhir/rant-signature.json index 9671148..d537519 100644 --- a/app/consent/fhir/rant-signature.json +++ b/app/consent/fhir/rant-signature.json @@ -6,6 +6,33 @@ "subjectType": [ "patient" ], + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-rant"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-rant", + "display": "ppm-rant" + } + ], + "text": "ppm-rant" + } + } + ], "item": [ { "linkId": "display-1", diff --git a/app/consent/forms.py b/app/consent/forms.py index b95ef3e..cc113ff 100644 --- a/app/consent/forms.py +++ b/app/consent/forms.py @@ -114,7 +114,7 @@ def _quiz_fields(questionnaire_id): label=question['text'], required=True, widget=forms.RadioSelect, - choices=tuple([(option['valueString'], option['valueString']) for option in question['option']]) + choices=tuple([(option['valueString'], option['valueString']) for option in question["answerOption"]]) ) return fields diff --git a/app/consent/management/commands/updatefhirconsents.py b/app/consent/management/commands/updatefhirconsents.py index 9b387a5..9335bf1 100644 --- a/app/consent/management/commands/updatefhirconsents.py +++ b/app/consent/management/commands/updatefhirconsents.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, CommandError from consent.apps import ConsentConfig -from fhirquestionnaire.fhir import FHIR +from ppmutils.fhir import FHIR class Command(BaseCommand): @@ -35,7 +35,7 @@ def handle(self, *args, **options): name = '{}/{}'.format(resource['resourceType'], resource['id']) # Do the update - if FHIR.update_resource(resource): + if FHIR.fhir_create(resource['resourceType'], resource, resource['id']): self.stdout.write(self.style.SUCCESS('Successfully updated FHIR resource: {}'.format(name))) else: @@ -44,5 +44,5 @@ def handle(self, *args, **options): except ValueError: raise CommandError('Resource could not be read: {}'.format(file_path)) - except KeyError: - raise CommandError('FHIR Resource does not contain ID and/or ResourceType: {}'.format(file_path)) + except KeyError as e: + raise CommandError('FHIR Resource \'{}\' does not contain ID and/or ResourceType: {}'.format(file_path, e)) diff --git a/app/consent/templates/consent/example/_consent.html b/app/consent/templates/consent/example/_consent.html index 03826e6..316896d 100644 --- a/app/consent/templates/consent/example/_consent.html +++ b/app/consent/templates/consent/example/_consent.html @@ -101,7 +101,7 @@

  • We will ask you if you are willing to participate in future surveys/questionnaires and seek your permission to contact you with additional questionnaires related to this study. These will take no longer than 10-15 minutes each and you may choose whether you wish to answer each questionnaire as you receive it. Questionnaires will be emailed to you with an explanation and a link to access the questionnaire online.
  • -
  • In addition to this consent, participants may be asked to wear a Fitbit® for the purposes of this study. Those who agree to wear one must agree to the Fitbit® terms of service, end user license agreement, and privacy policy. If you choose to wear a Fitbit®, you will be required to create a Fitbit® account on Fitbit.com and agree to the terms of service listed (https://www.fitbit.com/legal/terms-of-service). If you do not agree to the terms of service, please opt out of wearing the Fitbit® at the end of this consent form. +
  • In addition to this consent, participants may be asked to wear a Fitbit® for the purposes of this study. Those who agree to wear one must agree to the Fitbit® terms of service, end user license agreement, and privacy policy. If you choose to wear a Fitbit®, you will be required to create a Fitbit® account on Fitbit.com and agree to the terms of service listed (https://www.fitbit.com/legal/terms-of-service). If you do not agree to the terms of service, please opt out of wearing the Fitbit® at the end of this consent form.
    • An account is required in order to gather the data from the Fitbit®. The data is stored in the Fitbit as it is being collected until it is uploaded to the Fitbit servers using the web application. After you grant us permission, we will obtain the data from Fitbit® servers on your behalf and store it alongside the other data we have collected for you.
    • diff --git a/app/consent/templates/consent/neer/_consent.html b/app/consent/templates/consent/neer/_consent.html index 2bf97f6..8c9a78a 100644 --- a/app/consent/templates/consent/neer/_consent.html +++ b/app/consent/templates/consent/neer/_consent.html @@ -103,7 +103,7 @@

    • We will ask you if you are willing to participate in future surveys/questionnaires and seek your permission to contact you with additional questionnaires related to this study. These will take no longer than 10-15 minutes each and you may choose whether you wish to answer each questionnaire as you receive it. Questionnaires will be emailed to you with an explanation and a link to access the questionnaire online.
    • -
    • In addition to this consent, participants may be asked to wear a Fitbit® for the purposes of this study. Those who agree to wear one must agree to the Fitbit® terms of service, end user license agreement, and privacy policy. If you choose to wear a Fitbit®, you will be required to create a Fitbit® account on Fitbit.com and agree to the terms of service listed (https://www.fitbit.com/legal/terms-of-service). If you do not agree to the terms of service, please opt out of wearing the Fitbit® at the end of this consent form. +
    • In addition to this consent, participants may be asked to wear a Fitbit® for the purposes of this study. Those who agree to wear one must agree to the Fitbit® terms of service, end user license agreement, and privacy policy. If you choose to wear a Fitbit®, you will be required to create a Fitbit® account on Fitbit.com and agree to the terms of service listed (https://www.fitbit.com/legal/terms-of-service). If you do not agree to the terms of service, please opt out of wearing the Fitbit® at the end of this consent form.
      • An account is required in order to gather the data from the Fitbit®. The data is stored in the Fitbit as it is being collected until it is uploaded to the Fitbit servers using the web application. After you grant us permission, we will obtain the data from Fitbit® servers on your behalf and store it alongside the other data we have collected for you.
      • diff --git a/app/consent/urls.py b/app/consent/urls.py index 9a1f530..cacb26b 100644 --- a/app/consent/urls.py +++ b/app/consent/urls.py @@ -8,7 +8,7 @@ urlpatterns = [ re_path(r'^p/(?P[a-z\-_]+)/$', views.StudyView.as_view(), name='study'), re_path(r'^d/(?P[a-z\-_]+)/$', views.DownloadView.as_view(), name='download'), - re_path(r'^d/(?P[a-z\-_]+)/(?P[\d]+)/$', views.AdminDownloadView.as_view(), name='admin-download'), + re_path(r'^d/(?P[a-z\-_]+)/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[\d]+)/$', views.AdminDownloadView.as_view(), name='admin-download'), re_path(r'^c/asd/$', views.ASDView.as_view(), name='asd'), re_path(r'^c/asd/quiz/$', views.ASDQuizView.as_view(), name='asd-quiz'), diff --git a/app/fhirquestionnaire/fhir.py b/app/fhirquestionnaire/fhir.py index c9a109c..102d55f 100644 --- a/app/fhirquestionnaire/fhir.py +++ b/app/fhirquestionnaire/fhir.py @@ -1,32 +1,11 @@ -import requests import datetime -import uuid -import hashlib -import base64 -from urllib.parse import quote, urlencode +from urllib.parse import quote from django.template.loader import render_to_string -from django.conf import settings -from fhirclient import client -from fhirclient.models.bundle import Bundle, BundleEntry -from fhirclient.models.fhirreference import FHIRReference -from fhirclient.models.fhirdate import FHIRDate from fhirclient.models.patient import Patient -from fhirclient.models.humanname import HumanName -from fhirclient.models.relatedperson import RelatedPerson -from fhirclient.models.coding import Coding -from fhirclient.models.fhirelementfactory import FHIRElementFactory -from fhirclient.models.narrative import Narrative -from fhirclient.models.codeableconcept import CodeableConcept -from fhirclient.models.period import Period -from fhirclient.models.consent import Consent, ConsentExcept, ConsentPolicy -from fhirclient.models.contract import Contract, ContractSigner -from fhirclient.models.signature import Signature -from fhirclient.models.composition import Composition, CompositionSection -from fhirclient.models.bundle import Bundle, BundleEntry, BundleEntryRequest -from fhirclient.models.questionnaire import Questionnaire, QuestionnaireItem -from fhirclient.models.questionnaireresponse import QuestionnaireResponse, QuestionnaireResponseItem, QuestionnaireResponseItemAnswer +from fhirclient.models.questionnaire import Questionnaire +from fhirclient.models.bundle import Bundle from ppmutils.ppm import PPM from ppmutils.fhir import FHIR as PPMFHIR @@ -40,10 +19,6 @@ class FHIR: input_type_system = 'https://peoplepoweredmedicine.org/questionnaire/input/type' input_range_system = 'https://peoplepoweredmedicine.org/questionnaire/input/range' - @staticmethod - def client(): - return client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - @staticmethod def submit_consent(study, patient_email, form, pdf=None, dry=False): """ @@ -67,11 +42,14 @@ def submit_consent(study, patient_email, form, pdf=None, dry=False): codes = data.get('exceptions', []) name = data['name'] signature = data['signature'] - date = data['date'].isoformat() + date = data['date'] # Build a list of resources to create resources = [] + # Get the current timestamp + timestamp = datetime.datetime.now(datetime.timezone.utc) + # Check if consent has questionnaire if PPM.Questionnaire.consent_questionnaire_for_study(study): @@ -86,20 +64,20 @@ def submit_consent(study, patient_email, form, pdf=None, dry=False): answers = {linkId: code in codes for linkId, code in exception_codes.items()} # Create needed resources - questionnaire_response = FHIR._questionnaire_response(questionnaire, patient, date, answers) - contract = FHIR._contract(patient, date, name, signature, questionnaire_response) - exceptions = FHIR._consent_exceptions(codes) - consent = FHIR._consent(patient, date, exceptions) + questionnaire_response = PPMFHIR.Resources.questionnaire_response(questionnaire, patient, date, answers) + contract = PPMFHIR.Resources.contract(patient, timestamp, name, signature, questionnaire_response) + exceptions = PPMFHIR.Resources.consent_exceptions(codes) + consent = PPMFHIR.Resources.consent(patient, timestamp, exceptions) resources.extend([questionnaire_response, consent, contract]) else: # Get patient - patient = FHIR.get_patient(patient_email) + patient = Patient(PPMFHIR.get_patient(patient_email)) # Create needed resources - contract = FHIR._contract(patient, date, name, signature) - consent = FHIR._consent(patient, date) + contract = PPMFHIR.Resources.contract(patient, timestamp, name, signature) + consent = PPMFHIR.Resources.consent(patient, timestamp) resources.extend([consent, contract]) @@ -107,19 +85,19 @@ def submit_consent(study, patient_email, form, pdf=None, dry=False): text = '
        {}
        '.format(render_to_string('consent/{}/_consent.html'.format(study))) # Generate composition - composition = FHIR._composition(patient, date, text, [consent, contract]) + composition = PPMFHIR.Resources.composition(patient, timestamp, text, [consent, contract]) # Add it to the resources resources.append(composition) # Bundle it into a transaction - bundle = FHIR._bundle(resources) + bundle = PPMFHIR.Resources.bundle(resources) # Save it if dry: logger.warning('PPM/{}: Dry mode, not persisting responses'.format(study)) else: - FHIR._post_bundle(bundle) + PPMFHIR.fhir_transaction(bundle.as_json()) @staticmethod def submit_questionnaire(study, patient_email, form, dry=False): @@ -140,19 +118,19 @@ def submit_questionnaire(study, patient_email, form, dry=False): questionnaire, patient = FHIR.get_resources(PPM.Questionnaire.questionnaire_for_study(study), patient_email, dry) # Just use now - date = datetime.datetime.utcnow().isoformat() + date = datetime.datetime.now(datetime.timezone.utc) # Build the response - questionnaire_response = FHIR._questionnaire_response(questionnaire, patient, date, form) + questionnaire_response = PPMFHIR.Resources.questionnaire_response(questionnaire, patient, date, form) # Bundle it into a transaction - bundle = FHIR._bundle([questionnaire_response]) + bundle = PPMFHIR.Resources.bundle([questionnaire_response]) # Save it if dry: logger.warning('PPM/{}: Dry mode, not persisting responses'.format(study)) else: - FHIR._post_bundle(bundle) + PPMFHIR.fhir_transaction(bundle.as_json()) @staticmethod def submit_asd_individual(patient_email, forms, dry=False): @@ -195,18 +173,21 @@ def submit_asd_individual(patient_email, forms, dry=False): # Check if this is testing/dry if not dry: logger.error("Patient could not be fetched", exc_info=True, extra={ - 'patient': FHIR._obfuscate_email(patient_email), + 'patient': FHIR.obfuscate_email(patient_email), }) raise FHIR.PatientDoesNotExist else: patient = FHIR.get_demo_patient(patient_email) + # Get the current timestamp + timestamp = datetime.datetime.now(datetime.timezone.utc) + # Get the exception codes from the form individual_form = forms['individual'] codes = individual_form.get('exceptions', []) name = individual_form['name'] signature = individual_form['signature'] - date = individual_form['date'].isoformat() + date = individual_form['date'] # Map exception codes to linkId exception_codes = { @@ -219,23 +200,23 @@ def submit_asd_individual(patient_email, forms, dry=False): answers = {linkId: code in codes for linkId, code in exception_codes.items()} # Create needed resources - quiz_questionnaire_response = FHIR._questionnaire_response(quiz_questionnaire, patient, date, forms['quiz']) - questionnaire_response = FHIR._questionnaire_response(signature_questionnaire, patient, date, answers) - contract = FHIR._contract(patient, date, name, signature, questionnaire_response) - exceptions = FHIR._consent_exceptions(codes) - consent = FHIR._consent(patient, date, exceptions) + quiz_questionnaire_response = PPMFHIR.Resources.questionnaire_response(quiz_questionnaire, patient, date, forms['quiz']) + questionnaire_response = PPMFHIR.Resources.questionnaire_response(signature_questionnaire, patient, date, answers) + contract = PPMFHIR.Resources.contract(patient, timestamp, name, signature, questionnaire_response) + exceptions = PPMFHIR.Resources.consent_exceptions(codes) + consent = PPMFHIR.Resources.consent(patient, timestamp, exceptions) # Get the signature HTML text = '
        {}
        '.format(render_to_string('consent/asd/_individual_consent.html')) # Generate composition - composition = FHIR._composition(patient, date, text, [consent, contract]) + composition = PPMFHIR.Resources.composition(patient, timestamp, text, [consent, contract]) # Bundle it into a transaction - bundle = FHIR._bundle([questionnaire_response, consent, contract, composition, quiz_questionnaire_response]) + bundle = PPMFHIR.Resources.bundle([questionnaire_response, consent, contract, composition, quiz_questionnaire_response]) # Save it - FHIR._post_bundle(bundle) + PPMFHIR.fhir_transaction(bundle.as_json()) @staticmethod def submit_asd_guardian(patient_email, forms, dry=False): @@ -284,7 +265,7 @@ def submit_asd_guardian(patient_email, forms, dry=False): # Check if this is testing/dry if not dry: logger.error("Patient could not be fetched", exc_info=True, extra={ - 'patient': FHIR._obfuscate_email(patient_email), + 'patient': FHIR.obfuscate_email(patient_email), }) raise FHIR.PatientDoesNotExist else: @@ -292,6 +273,9 @@ def submit_asd_guardian(patient_email, forms, dry=False): # Process the guardian's resources first + # Get the current timestamp + timestamp = datetime.datetime.now(datetime.timezone.utc) + # Get the forms quiz = forms['quiz'] ward_form = forms['ward'] @@ -311,7 +295,7 @@ def submit_asd_guardian(patient_email, forms, dry=False): # Get ward values from form ward_codes = ward_form['exceptions'] ward_signature = ward_form['signature'] - ward_date = ward_form['date'].isoformat() + ward_date = ward_form['date'] # Map exception codes to linkId guardian_exception_codes = { @@ -325,33 +309,33 @@ def submit_asd_guardian(patient_email, forms, dry=False): reason_answers = {'question-1': explained, 'question-1-1': reason} # Create the related person resource - related_person = FHIR._related_person(patient, guardian, relationship) + related_person = PPMFHIR.Resources.related_person(patient, guardian, relationship) # Create all questionnaire response resources - quiz_questionnaire_response = FHIR._questionnaire_response(quiz_questionnaire, patient, date, quiz, + quiz_questionnaire_response = PPMFHIR.Resources.questionnaire_response(quiz_questionnaire, patient, date, quiz, related_person) - guardian_signature_questionnaire_response = FHIR._questionnaire_response(guardian_signature_questionnaire, + guardian_signature_questionnaire_response = PPMFHIR.Resources.questionnaire_response(guardian_signature_questionnaire, patient, date, exception_answers, related_person) - guardian_explained_questionnaire_response = FHIR._questionnaire_response(guardian_reason_questionnaire, + guardian_explained_questionnaire_response = PPMFHIR.Resources.questionnaire_response(guardian_reason_questionnaire, patient, date, reason_answers, related_person) # Create contract resources - signature_contract = FHIR._related_contract(patient, date, name, related_person, guardian, + signature_contract = PPMFHIR.Resources.related_contract(patient, timestamp, name, related_person, guardian, signature, guardian_signature_questionnaire_response) - explained_contract = FHIR._related_contract(patient, date, name, related_person, guardian, + explained_contract = PPMFHIR.Resources.related_contract(patient, timestamp, name, related_person, guardian, explained_signature, guardian_explained_questionnaire_response) # Create the guardian's consent - exceptions = FHIR._consent_exceptions(guardian_codes) - consent = FHIR._consent(patient, date, exceptions, related_person) + exceptions = PPMFHIR.Resources.consent_exceptions(guardian_codes) + consent = PPMFHIR.Resources.consent(patient, timestamp, exceptions, related_person) # Get the signature HTML text = '
        {}
        '.format(render_to_string('consent/asd/_guardian_consent.html')) # Generate composition - composition = FHIR._composition(patient, date, text, [consent, signature_contract, explained_contract]) + composition = PPMFHIR.Resources.composition(patient, timestamp, text, [consent, signature_contract, explained_contract]) # Map exception codes to linkId ward_exception_codes = { @@ -364,15 +348,15 @@ def submit_asd_guardian(patient_email, forms, dry=False): # Create participant resources ward_exceptions = {linkId: code in ward_codes for linkId, code in ward_exception_codes.items()} - ward_signature_questionnaire_response = FHIR._questionnaire_response(ward_signature_questionnaire, + ward_signature_questionnaire_response = PPMFHIR.Resources.questionnaire_response(ward_signature_questionnaire, patient, date, ward_exceptions) # Create the contract and composition - ward_contract = FHIR._contract(patient, ward_date, name, ward_signature, ward_signature_questionnaire_response) - ward_composition = FHIR._composition(patient, ward_date, ward_text, [ward_contract]) + ward_contract = PPMFHIR.Resources.contract(patient, timestamp, name, ward_signature, ward_signature_questionnaire_response) + ward_composition = PPMFHIR.Resources.composition(patient, timestamp, ward_text, [ward_contract]) # Bundle it into a transaction - bundle = FHIR._bundle([related_person, + bundle = PPMFHIR.Resources.bundle([related_person, quiz_questionnaire_response, guardian_signature_questionnaire_response, guardian_explained_questionnaire_response, @@ -385,7 +369,7 @@ def submit_asd_guardian(patient_email, forms, dry=False): ward_composition]) # Save it - FHIR._post_bundle(bundle) + PPMFHIR.fhir_transaction(bundle.as_json()) @staticmethod def get_demo_patient(email): @@ -407,66 +391,6 @@ def get_demo_patient(email): ] }) - @staticmethod - def post_resource(json): - """ - Builds a FHIR resource from the passed JSON and posts it to the - current server. Can be any valid FHIR resource that already - exists in the server. - :param json: JSON FHIR Resource - :return: True if updated successfully, False otherwise - """ - # Track response for debugging - content = None - try: - # Post it - response = requests.post(settings.FHIR_URL, - headers={'content-type': 'application/json'}, - json=json) - content = response.content - response.raise_for_status() - - except Exception as e: - logger.error( - "Could not post resource: {}".format(e), - exc_info=True, extra={ - 'response': content, - } - ) - return False - - @staticmethod - def update_resource(json): - """ - Builds a FHIR resource from the passed JSON and updates the current server. Can be any valid - FHIR resource that already exists in the server. - :param json: JSON FHIR Resource - :return: True if updated successfully, False otherwise - """ - - try: - # Prepare the client - fhir = client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - - # Get the resource type - resource_type = json['resourceType'] - - # Create the resource - resource = FHIRElementFactory.instantiate(resource_type, json) - - # Save it - resource.update(fhir.server) - - logger.debug('Updated FHIR resource: {}/{}'.format(resource.resource_type, resource.id)) - - return True - - except Exception as e: - logger.error("Could not update resources: {}".format(e), exc_info=True, extra={ - 'resource': json, - }) - return False - @staticmethod def _query_resources(queries=[]): @@ -489,130 +413,52 @@ def _query_resources(queries=[]): # Query for a response # Execute the search - response = requests.post(settings.FHIR_URL, headers={'content-type': 'application/json'}, json=transaction) - response.raise_for_status() + response = PPMFHIR.fhir_transaction(transaction) # Build the objects - bundle = Bundle(response.json()) + bundle = Bundle(response) return bundle @staticmethod def get_resources(questionnaire_id, patient_email, dry=False): - # Build the transaction - transaction = { - 'resourceType': 'Bundle', - 'type': 'transaction', - 'entry': [] - } - # Get the questionnaire - transaction['entry'].append({ - 'request': { - 'url': 'Questionnaire?_id={}'.format(questionnaire_id), - 'method': 'GET' - } - }) - - # Add the request for the patient - transaction['entry'].append({ - 'request': { - 'url': 'Patient?identifier=http://schema.org/email|{}'.format(quote(patient_email)), - 'method': 'GET' - } - }) + questionnaire = Questionnaire(PPMFHIR.fhir_read("Questionnaire", questionnaire_id)) - # Query for a response - - # Execute the search - response = requests.post(settings.FHIR_URL, headers={'content-type': 'application/json'}, json=transaction) - response.raise_for_status() - - # Build the objects - bundle = Bundle(response.json()) - - # Check for the questionnaire - if not bundle.entry[0].resource.entry or bundle.entry[0].resource.entry[0].resource.resource_type != 'Questionnaire': - logger.error("Questionnaire could not be fetched", exc_info=True, extra={ - 'questionnaires': questionnaire_id, - }) - raise FHIR.QuestionnaireDoesNotExist() - - # Instantiate it - questionnaire = bundle.entry[0].resource.entry[0].resource + # Search for the patient + patient = Patient(PPMFHIR.get_patient(patient_email)) # Check for the patient if not dry: - if not bundle.entry[1].resource.entry or bundle.entry[1].resource.entry[0].resource.resource_type != 'Patient': + if not patient: logger.error("Patient could not be fetched", exc_info=True, extra={ - 'patient': FHIR._obfuscate_email(patient_email), + 'patient': FHIR.obfuscate_email(patient_email), 'questionnaires': questionnaire_id, - 'bundle': bundle.as_json(), }) raise FHIR.PatientDoesNotExist() - - # Get it - patient = bundle.entry[1].resource.entry[0].resource else: # In dry mode, use a fake patient patient = FHIR.get_demo_patient(patient_email) return questionnaire, patient - @staticmethod - def get_patient(patient_email): - - # Build the transaction - transaction = { - 'resourceType': 'Bundle', - 'type': 'transaction', - 'entry': [] - } - - # Add the request for the patient - transaction['entry'].append({ - 'request': { - 'url': 'Patient?identifier=http://schema.org/email|{}'.format(quote(patient_email)), - 'method': 'GET' - } - }) - - # Query for a response - - # Execute the search - response = requests.post(settings.FHIR_URL, headers={'content-type': 'application/json'}, json=transaction) - response.raise_for_status() - - # Build the objects - bundle = Bundle(response.json()) - - # Check for the Patient - if not bundle.entry[0].resource.entry or bundle.entry[0].resource.entry[0].resource.resource_type != 'Patient': - logger.error("Patient could not be fetched", exc_info=True, extra={ - 'patient': FHIR._obfuscate_email(patient_email), - }) - raise FHIR.PatientDoesNotExist() - - # Instantiate it - patient = bundle.entry[0].resource.entry[0].resource - - return patient - @staticmethod def check_patient(patient_email): + """ + Checks FHIR for a Patient resource for the given email. - # Prepare the client - fhir = client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - - # Set the search parameters - struct = {'identifier': 'http://schema.org/email|{}'.format(patient_email)} + :param patient_email: The email of the participant to check + :type patient_email: str + :raises FHIR.PatientDoesNotExist: If does not exist + """ - # Get the questionnaire - search = Patient.where(struct=struct) - resources = search.perform_resources(fhir.server) - if not resources: - logger.warning('Patient not found: {}'.format(FHIR._obfuscate_email(patient_email))) + # Search for Patient + if not PPMFHIR.get_patient(patient_email): + logger.warning( + f"PPM/{FHIR.obfuscate_email(patient_email)}: " + "Patient NOT found" + ) raise FHIR.PatientDoesNotExist @staticmethod @@ -629,69 +475,40 @@ def check_consent(study, patient_email): """ # Search for the consent composition resource specific to this study - resources = PPMFHIR.get_consent_composition(patient_email, study) - if resources: - logger.debug(f"PPM/{study}/{FHIR._obfuscate_email(patient_email)}: Consent composition already exists") + if PPMFHIR.get_consent_composition(patient_email, study): + logger.debug( + f"PPM/{study}/{FHIR.obfuscate_email(patient_email)}: " + "Consent composition already exists" + ) raise FHIR.ConsentAlreadyExists return False @staticmethod def check_response(questionnaire_id, patient_email): - - # Prepare the client - fhir = client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - - # Set the search parameters - struct = {'questionnaire': questionnaire_id, - 'source:Patient.identifier': 'http://schema.org/email|{}'.format(patient_email)} - - # Get the questionnaire - search = QuestionnaireResponse.where(struct=struct) - resources = search.perform_resources(fhir.server) - if resources: - logger.warning('Questionnaire not found: {}'.format(questionnaire_id)) - raise FHIR.QuestionnaireResponseAlreadyExists - - @staticmethod - def check_responses(questionnaire_ids, patient_email): - - # Prepare the client - fhir = client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - - for questionnaire_id in questionnaire_ids: - - # Set the search parameters - struct = {'questionnaire': questionnaire_id, - 'source:Patient.identifier': 'http://schema.org/email|{}'.format(patient_email)} - - # Get the questionnaire - search = QuestionnaireResponse.where(struct=struct) - resources = search.perform_resources(fhir.server) - if resources: - raise FHIR.QuestionnaireResponseAlreadyExists - - @staticmethod - def _reference_to(resource): - """ - Used to create a FHIR reference object based on a FHIRClient.models object - :param resource: FHIRClient.models class object (i.e. Patient()) - :returns: FHIRReference object """ - reference = FHIRReference() - reference.reference = '{}/{}'.format(resource.resource_type, resource.id) - return reference + Checks FHIR for resources pertaining to a response for this + specific questionnaire. - @staticmethod - def _reference(resource_type, resource_id): - """ - Used to create a FHIR reference object based on a FHIRClient.models object - :param resource: FHIRClient.models class object (i.e. Patient()) - :returns: FHIRReference object + :param questionnaire_id: The questionnaire for which the response was made + :type questionnaire_id: str + :param patient_email: The email of the participant to check + :type patient_email: str + :raises FHIR.QuestionnaireResponseAlreadyExists: If exists already """ - reference = FHIRReference({'reference': '{}/{}'.format(resource_type, resource_id)}) - return reference + # Get the questionnaire + if PPMFHIR.get_questionnaire_response(patient_email, questionnaire_id): + logger.warning( + f"PPM/{FHIR.obfuscate_email(patient_email)}: " + f"QuestionnaireResponse found for '{questionnaire_id}'" + ) + raise FHIR.QuestionnaireResponseAlreadyExists + else: + logger.debug( + f"PPM/{FHIR.obfuscate_email(patient_email)}: " + f"QuestionnaireResponse NOT found for '{questionnaire_id}'" + ) class QuestionnaireDoesNotExist(Exception): pass @@ -706,413 +523,7 @@ class ConsentAlreadyExists(Exception): pass @staticmethod - def _questionnaire_response(questionnaire, patient, date=datetime.datetime.utcnow().isoformat(), answers={}, author=None): - - # Build the response - response = QuestionnaireResponse() - response.id = uuid.uuid4().urn - response.questionnaire = FHIR._reference_to(questionnaire) - response.source = FHIR._reference_to(patient) - response.status = 'completed' - response.authored = FHIRDate(date) - response.author = FHIR._reference_to(author if author else patient) - - # Collect response items flattened - response.item = FHIR._questionnaire_response_items(questionnaire, answers) - - # Set it on the questionnaire - return response - - @staticmethod - def _questionnaire_response_items(questionnaire_item, form): - - # Collect items - items = [] - - # Iterate through questions - for question in questionnaire_item.item: - - # Determine if this is a required item or a dependently required item - dependent = question.enableWhen or getattr(questionnaire_item, 'enableWhen', False) - - # Disregard invalid question types - if not question.linkId or not question.type or question.type == 'display': - continue - - # If a group, process subelements and append them to current level - elif question.type == 'group': - - # We don't respect heirarchy for groupings - group_items = FHIR._questionnaire_response_items(question, form) - if group_items: - items.extend(group_items) - continue - - # Get the value - value = form.get(question.linkId, form.get(question.linkId, None)) - - # Add the item - if value is None or not str(value): - if question.required and not dependent: - logger.warning('PPM/{}: No answer for {}'.format(form.get('questionnaire_id'), question.linkId)) - continue - - # Check for an empty list - elif type(value) is list and len(value) == 0: - if question.required and not dependent: - logger.warning('PPM/{}: Empty answer set for {}'.format(form.get('questionnaire_id'), question.linkId)) - continue - - # Create the item - response_item = FHIR._questionnaire_response_item(question.linkId, value) - - # Add the item - items.append(response_item) - - # Check for subitems - if question.item: - - # Get the items - question_items = FHIR._questionnaire_response_items(question, form) - if question_items: - # TODO: Uncomment the following line after QuestionnaireResponse parsing is updated to - # TODO: look for subanswers in subitems as opposed to one flat list - #item.item = question_items - - # Save all answers flat for now - items.extend(question_items) - - return items - - @staticmethod - def _questionnaire_response_item(link_id, cleaned_data): - - # Create the response item - item = QuestionnaireResponseItem() - item.linkId = link_id - - # Create the answer items list - item.answer = [] - - # Check the value type - if type(cleaned_data) is list: - - # Add each item in the list - for value in cleaned_data: - item.answer.append(FHIR._questionnaire_response_item_answer(value)) - - else: - - # Add the single item - item.answer.append(FHIR._questionnaire_response_item_answer(cleaned_data)) - - return item - - @staticmethod - def _questionnaire_response_item_answer(value): - - # Create the item - answer = QuestionnaireResponseItemAnswer() - - # Check type - if type(value) is str: - answer.valueString = str(value) - - elif type(value) is bool: - answer.valueBoolean = value - - elif type(value) is int: - answer.valueInteger = value - - elif type(value) is datetime.datetime: - answer.valueDateTime = FHIRDate(value.isoformat()) - - elif type(value) is datetime.date: - answer.valueDate = FHIRDate(value.isoformat()) - - else: - logger.warning('Unhandled answer type: {} - {}'.format(type(value), value)) - - # Cast it as string - answer.valueString = str(value) - - return answer - - @staticmethod - def _consent_exceptions(codes): - - # Map codes to displays - displays = { - '284036006': 'Equipment monitoring', - '702475000': 'Referral to clinical trial', - '82078001': 'Taking blood sample', - '165334004': 'Stool sample sent to lab', - '258435002': 'Tumor tissue sample', - '225098009': 'Collection of sample of saliva', - } - - # Collect exceptions - exceptions = [] - for code in codes: - - # Create the exception - exception = ConsentExcept() - exception.type = "deny" - exception.code = [FHIR._coding("http://snomed.info/sct", code, displays[code])] - - # Add it - exceptions.append(exception) - - return exceptions - - @staticmethod - def _related_person(patient, name, relationship): - - # Make it - person = RelatedPerson() - person.id = uuid.uuid4().urn - person.patient = FHIR._reference_to(patient) - - # Set the relationship - code = CodeableConcept() - code.text = relationship - person.relationship = code - - # Set the name - human_name = HumanName() - human_name.text = name - person.name = [human_name] - - return person - - @staticmethod - def _consent(patient, date, exceptions=[], related_person=None): - - # Make it - consent = Consent() - consent.status = 'proposed' - consent.id = uuid.uuid4().urn - consent.dateTime = FHIRDate(date) - consent.patient = FHIR._reference_to(patient) - - # Policy - policy = ConsentPolicy() - policy.authority = 'HMS-DBMI' - policy.uri = 'https://hms.harvard.edu' - consent.policy = [policy] - - # Check for a related person consenting - if related_person: - - # Add them - consent.consentingParty = [FHIR._reference_to(related_person)] - - # Period - period = Period() - period.start = FHIRDate(date) - - # Add items - consent.period = period - consent.purpose = [FHIR._coding('http://hl7.org/fhir/v3/ActReason', 'HRESCH', 'healthcare research')] - - # Add exceptions - consent.except_fhir = exceptions - - return consent - - @staticmethod - def _contract(patient, date, patient_name, patient_signature, questionnaire_response=None): - - # Build it - contract = Contract() - contract.status = 'executed' - contract.issued = FHIRDate(date) - contract.id = uuid.uuid4().urn - - # Signer - signer = ContractSigner() - signer.type = FHIR._coding('http://hl7.org/fhir/ValueSet/contract-signer-type', 'CONSENTER', 'Consenter') - signer.party = FHIR._reference_to(patient) - - # Signature - signature = Signature() - signature.type = [FHIR._coding('http://hl7.org/fhir/ValueSet/signature-type', '1.2.840.10065.1.12.1.7', 'Consent Signature')] - signature.when = FHIRDate(date) - signature.contentType = 'text/plain' - signature.blob = FHIR._blob(patient_signature) - signature.whoReference = FHIR._reference_to(patient) - signature.whoReference.display = patient_name - - # Add references - signer.signature = [signature] - contract.signer = [signer] - - # Add questionnaire if passed - if questionnaire_response: - contract.bindingReference = FHIR._reference_to(questionnaire_response) - - return contract - - @staticmethod - def _related_contract(patient, date, patient_name, related_person, related_person_name, - related_person_signature, questionnaire_response): - - # Build it - contract = Contract() - contract.status = 'executed' - contract.issued = FHIRDate(date) - contract.id = uuid.uuid4().urn - - # Signer - contract_signer = ContractSigner() - contract_signer.type = FHIR._coding('http://hl7.org/fhir/ValueSet/contract-signer-type', 'CONSENTER', 'Consenter') - contract_signer.party = FHIR._reference_to(related_person) - - # Signature - signature = Signature() - signature.type = [FHIR._coding('http://hl7.org/fhir/ValueSet/signature-type', '1.2.840.10065.1.12.1.7', 'Consent Signature')] - signature.when = FHIRDate(date) - signature.contentType = 'text/plain' - signature.blob = FHIR._blob(related_person_signature) - signature.whoReference = FHIR._reference_to(related_person) - signature.whoReference.display = related_person_name - - # Refer to the patient - patient_reference = FHIRReference({'reference': '{}/{}'.format(patient.resource_type, patient.id), - 'display': patient_name}) - signature.onBehalfOfReference = patient_reference - - # Add references - contract_signer.signature = [signature] - contract.signer = [contract_signer] - contract.bindingReference = FHIR._reference_to(questionnaire_response) - - return contract - - @staticmethod - def _composition(patient, date, text, resources=[]): - - # Build it - composition = Composition() - composition.id = uuid.uuid4().urn - composition.status = 'final' - composition.subject = FHIR._reference_to(patient) - composition.date = FHIRDate(date) - composition.title = 'Signature' - composition.author = [FHIRReference({'reference': 'Device/hms-dbmi-ppm-consent'})] - - # Composition type - coding = Coding() - coding.system = 'http://loinc.org' - coding.code = '83930-8' - coding.display = 'Research Consent' - - # Convoluted code property - code = CodeableConcept() - code.coding = [coding] - - # Combine - composition.type = code - - # Add sections - composition.section = [] - - # Add text - narrative = Narrative() - narrative.div = text - narrative.status = 'additional' - text_section = CompositionSection() - text_section.text = narrative - composition.section.append(text_section) - - # Add related section resources - for resource in resources: - - # Add consent - consent_section = CompositionSection() - consent_section.entry = [FHIR._reference_to(resource)] - composition.section.append(consent_section) - - return composition - - @staticmethod - def _code(system, code, display): - - # Build it - coding = Coding() - coding.system = system - coding.code = code - coding.display = display - - # Convoluted code property - codeable = CodeableConcept() - codeable.coding = [coding] - - return codeable - - @staticmethod - def _coding(system, code, display): - - # Build it - coding = Coding() - coding.system = system - coding.code = code - coding.display = display - - return coding - - @staticmethod - def _blob(value): - - # Base64 encode it - return base64.b64encode(str(value).encode('utf-8')).decode('utf-8') - - @staticmethod - def _bundle(resources): - - # Build the bundle - bundle = Bundle() - bundle.type = 'transaction' - bundle.entry = [] - - for resource in resources: - - # Build the entry request - bundle_entry_request = BundleEntryRequest() - bundle_entry_request.method = 'POST' - bundle_entry_request.url = resource.resource_type - - # Add it to the entry - bundle_entry = BundleEntry() - bundle_entry.resource = resource - bundle_entry.fullUrl = resource.id - bundle_entry.request = bundle_entry_request - - # Add it - bundle.entry.append(bundle_entry) - - return bundle - - @staticmethod - def _post_bundle(bundle): - - # Track response for debugging - content = None - try: - # Post it - response = requests.post(settings.FHIR_URL, - headers={'content-type': 'application/json'}, - json=bundle.as_json()) - content = response.content - response.raise_for_status() - - except Exception as e: - logger.error("Could not post bundle: {}".format(e), exc_info=True, extra={ - 'response': content, - }) - - @staticmethod - def _obfuscate_email(email): + def obfuscate_email(email): # Parts parts = email.split('@') diff --git a/app/fhirquestionnaire/settings.py b/app/fhirquestionnaire/settings.py index d1f19c4..efc7d1f 100644 --- a/app/fhirquestionnaire/settings.py +++ b/app/fhirquestionnaire/settings.py @@ -188,8 +188,6 @@ } # App configurations -FHIR_APP_ID = get_str("FHIR_APP_ID", required=True) -FHIR_URL = get_str("FHIR_URL", required=True) RETURN_URL = get_str("RETURN_URL", required=True) PPM_P2MD_URL = get_str("PPM_P2MD_URL", required=True) diff --git a/app/questionnaire/fhir/asd.json b/app/questionnaire/fhir/asd.json index 55b99db..8785707 100644 --- a/app/questionnaire/fhir/asd.json +++ b/app/questionnaire/fhir/asd.json @@ -9,6 +9,33 @@ } ] }, + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-asd"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-asd", + "display": "ppm-asd" + } + ], + "text": "ppm-asd" + } + } + ], "title": "PPM ASD Questionnaire", "status": "active", "date": "2016-12-21", @@ -19,9 +46,9 @@ { "linkId": "question-1", "text": "Did you join the PPM pilot because you have autism or your child has autism?", - "type": "question", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "I have autism" }, @@ -38,9 +65,9 @@ { "linkId": "question-2", "text": "Who else has autism spectrum disorder in your family?", - "type": "question", + "type": "choice", "repeats": true, - "option": [ + "answerOption": [ { "valueString": "My son has autism" }, @@ -85,9 +112,9 @@ { "linkId": "question-3", "text": "Do any of the family members checked above have autism AND any of the following disorders? Please indicate which family member from the list above has any of the following:", - "type": "question", + "type": "choice", "repeats": true, - "option": [ + "answerOption": [ { "valueString": "Irritable Bowel Syndrome (IBS)" }, @@ -118,7 +145,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Irritable Bowel Syndrome (IBS)" + "answerString": "Irritable Bowel Syndrome (IBS)", + "operator": "=" } ], "required": true @@ -130,7 +158,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Inflammatory Bowel Disease (IBD)" + "answerString": "Inflammatory Bowel Disease (IBD)", + "operator": "=" } ], "required": true @@ -142,7 +171,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Rheumatoid Arthritis" + "answerString": "Rheumatoid Arthritis", + "operator": "=" } ], "required": true @@ -154,7 +184,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Type 1 diabetes" + "answerString": "Type 1 diabetes", + "operator": "=" } ] }, @@ -165,7 +196,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Scleroderma" + "answerString": "Scleroderma", + "operator": "=" } ], "required": true @@ -177,7 +209,8 @@ "enableWhen": [ { "question": "question-3", - "answerString": "Lupus" + "answerString": "Lupus", + "operator": "=" } ], "required": true @@ -187,9 +220,9 @@ { "linkId": "question-4", "text": "Does the Subject take any of the following medications:", - "type": "question", + "type": "choice", "repeats": true, - "option": [ + "answerOption": [ { "valueString": "Anti-anxiety medications" }, @@ -214,7 +247,8 @@ "enableWhen": [ { "question": "question-4", - "answerString": "Anti-anxiety medications" + "answerString": "Anti-anxiety medications", + "operator": "=" } ], "required": true @@ -226,7 +260,8 @@ "enableWhen": [ { "question": "question-4", - "answerString": "Attention Deficit Disorder (ADHD) medications" + "answerString": "Attention Deficit Disorder (ADHD) medications", + "operator": "=" } ], "required": true @@ -238,7 +273,8 @@ "enableWhen": [ { "question": "question-4", - "answerString": "Anti-inflammatory medications" + "answerString": "Anti-inflammatory medications", + "operator": "=" } ], "required": true @@ -250,7 +286,8 @@ "enableWhen": [ { "question": "question-4", - "answerString": "Any other medications" + "answerString": "Any other medications", + "operator": "=" } ], "required": true @@ -262,7 +299,8 @@ "enableWhen": [ { "question": "question-4", - "answerString": "Vitamins" + "answerString": "Vitamins", + "operator": "=" } ], "required": true diff --git a/app/questionnaire/fhir/neer.json b/app/questionnaire/fhir/neer.json index 74c3b22..55cad62 100644 --- a/app/questionnaire/fhir/neer.json +++ b/app/questionnaire/fhir/neer.json @@ -9,6 +9,33 @@ } ] }, + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program", + "display": "Program" + }, + "valueReference": {"reference": "ResearchStudy/ppm-neer"} + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "task", + "display": "Workflow Task" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "https://peoplepoweredmedicine.org/fhir/questionnaire/context", + "code": "ppm-neer", + "display": "ppm-neer" + } + ], + "text": "ppm-neer" + } + } + ], "status": "active", "date": "2017-06-07", "subjectType": [ @@ -36,9 +63,9 @@ { "linkId": "question-8", "text": "Gender", - "type": "text", + "type": "choice", "required": true, - "option": [ + "answerOption": [ { "valueString": "Male" }, @@ -63,7 +90,8 @@ "enableWhen": [ { "question": "question-9", - "answerBoolean": true + "answerBoolean": true, + "operator": "=" } ], "required": true @@ -75,7 +103,8 @@ "enableWhen": [ { "question": "question-9", - "answerBoolean": true + "answerBoolean": true, + "operator": "=" } ], "required": true @@ -150,7 +179,7 @@ { "linkId": "question-22", "text": "Why do you describe yourself as a “super” responder:", - "initialString": "Please provide a detailed answer, as this will help us determine your eligibility for the study", + "initial": [{"valueString": "Please provide a detailed answer, as this will help us determine your eligibility for the study"}], "type": "text", "required": true }, diff --git a/app/questionnaire/forms.py b/app/questionnaire/forms.py index cf87eb0..3e876c2 100644 --- a/app/questionnaire/forms.py +++ b/app/questionnaire/forms.py @@ -10,10 +10,9 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput, MonthPickerInput from django.utils.translation import gettext_lazy as _ -from fhirclient import client -from fhirclient.server import FHIRNotFoundException from fhirclient.models.questionnaire import Questionnaire from ppmutils.ppm import PPM +from ppmutils.fhir import FHIR as PPMFHIR from fhirquestionnaire.fhir import FHIR @@ -50,17 +49,15 @@ def __init__(self, questionnaire_id, *args, **kwargs): super(FHIRQuestionnaireForm, self).__init__(*args, **kwargs) try: - # Prepare the client - fhir = client.FHIRClient(settings={'app_id': settings.FHIR_APP_ID, 'api_base': settings.FHIR_URL}) - # Get the questionnaire - questionnaire = Questionnaire.read(questionnaire_id, fhir.server) + questionnaire = Questionnaire(PPMFHIR.fhir_read("Questionnaire", questionnaire_id)) # Retain it self.questionnaire_id = questionnaire_id self.questionnaire = questionnaire - except FHIRNotFoundException: + except Exception as e: + logger.exception(f"PPM/FHIR: Questionnaire/{questionnaire_id} not found: {e}", exc_info=True) raise FHIR.QuestionnaireDoesNotExist # Get fields @@ -159,11 +156,11 @@ def _get_form_fields(self, parent, questionnaire_id): attrs.update(self._get_form_layout_item_attributes(item, enable_when)) # Check type - if (item.type == 'string' or item.type == 'text') and item.option: + if (item.type == 'string' or item.type == 'text') and item.answerOption: # Set the choices choices = () - for option in item.option: + for option in item.answerOption: # Assume valueString if not option.valueString: @@ -180,16 +177,16 @@ def _get_form_fields(self, parent, questionnaire_id): required=required ) - elif item.type == 'string' or item.type == 'text' or (item.type == 'question' and not item.option): + elif item.type == 'string' or item.type == 'text' or (item.type == 'question' and not item.answerOption): - if item.initialString: + if item.initial: # Make this a textbox-style input with minimum width fields[item.linkId] = forms.CharField( label=item.text, required=required, widget=forms.Textarea(attrs={**{ - 'placeholder': item.initialString, + 'placeholder': next((i.valueString for i in item.initial), ""), 'pattern': ".{6,}", 'title': 'Please be as descriptive as possible for this question', 'oninvalid': "setCustomValidity('Please be as " @@ -290,7 +287,7 @@ def _get_form_fields(self, parent, questionnaire_id): # Set the choices choices = () - for option in item.option: + for option in item.answerOption: # Assume valueString if not option.valueString: @@ -299,19 +296,30 @@ def _get_form_fields(self, parent, questionnaire_id): else: choices = choices + ((option.valueString, option.valueString),) - # Set the input - fields[item.linkId] = forms.TypedChoiceField( - label=item.text, - choices=choices, - widget=forms.CheckboxSelectMultiple(attrs=attrs), - required=required - ) + # Check if repeats + if item.repeats: + # Set the input + fields[item.linkId] = forms.MultipleChoiceField( + label=item.text, + choices=choices, + widget=forms.CheckboxSelectMultiple(attrs=attrs), + required=required + ) + + else: + # Set the input + fields[item.linkId] = forms.TypedChoiceField( + label=item.text, + choices=choices, + widget=forms.RadioSelect(attrs=attrs), + required=required + ) - elif item.type == 'question' and item.option: + elif item.type == 'question' and item.answerOption: # Set the choices choices = () - for option in item.option: + for option in item.answerOption: # Assume valueString if not option.valueString: diff --git a/app/questionnaire/management/commands/updatefhirquestionnaires.py b/app/questionnaire/management/commands/updatefhirquestionnaires.py index 34e164a..45acba0 100644 --- a/app/questionnaire/management/commands/updatefhirquestionnaires.py +++ b/app/questionnaire/management/commands/updatefhirquestionnaires.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, CommandError from questionnaire.apps import QuestionnaireConfig -from fhirquestionnaire.fhir import FHIR +from ppmutils.fhir import FHIR class Command(BaseCommand): @@ -35,7 +35,7 @@ def handle(self, *args, **options): name = '{}/{}'.format(resource['resourceType'], resource['id']) # Do the update - if FHIR.update_resource(resource): + if FHIR.fhir_create(resource['resourceType'], resource, resource['id']): self.stdout.write(self.style.SUCCESS('Successfully updated FHIR resource: {}'.format(name))) else: @@ -44,5 +44,5 @@ def handle(self, *args, **options): except ValueError: raise CommandError('Resource could not be read: {}'.format(file_path)) - except KeyError: - raise CommandError('FHIR Resource does not contain ID and/or ResourceType: {}'.format(file_path)) + except KeyError as e: + raise CommandError('FHIR Resource \'{}\' does not contain ID and/or ResourceType: {}'.format(file_path, e)) From f46e2e28bd9cd4eba7b40a08edcf41e48ea76da0 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 25 Mar 2024 09:42:14 -0600 Subject: [PATCH 2/4] PPM-766 - Updated ppmutils version --- requirements.in | 2 +- requirements.txt | 100 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/requirements.in b/requirements.in index 934b095..b24bda1 100644 --- a/requirements.in +++ b/requirements.in @@ -6,6 +6,6 @@ django-dbmi-client<2.0 django-health-check<4.0 django-ses<4.0 djangorestframework<4.0 -ppm-utils==0.15.3 +ppm-utils<3.0 raven<7.0 requests<3.0 diff --git a/requirements.txt b/requirements.txt index b8164d6..26fe2b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,13 +11,19 @@ asgiref==3.8.1 \ boto3==1.34.69 \ --hash=sha256:2e25ef6bd325217c2da329829478be063155897d8d3b29f31f7f23ab548519b1 \ --hash=sha256:898a5fed26b1351352703421d1a8b886ef2a74be6c97d5ecc92432ae01fda203 - # via django-ses + # via + # django-ses + # ppm-utils botocore==1.34.69 \ --hash=sha256:d1ab2bff3c2fd51719c2021d9fa2f30fbb9ed0a308f69e9a774ac92c8091380a \ --hash=sha256:d3802d076d4d507bf506f9845a6970ce43adc3d819dd57c2791f5c19ed6e5950 # via # boto3 # s3transfer +cachetools==5.3.3 \ + --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ + --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 + # via google-auth certifi==2024.2.2 \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 @@ -244,9 +250,9 @@ djangorestframework==3.15.1 \ # via # -r requirements.in # django-dbmi-client -fhirclient==3.2.0 \ - --hash=sha256:9d9fd8d54e4d51ab8f0898aaf7d5975ba984a95c6f81a198bfcd944bf7f1ce61 \ - --hash=sha256:fc85b7bd3c971b18779ae2ec0f3f5dc667eab16a7928e99d68c1c946613cc753 +fhirclient==4.1.0 \ + --hash=sha256:4f64f7967c9fe5f74cce068f29b0e5e4485848a73ec327765a8f3c188b9948a4 \ + --hash=sha256:fe9c92b3649a4d2829d91c40cd7765c6f729971d12d1dec39a7ee6e81f83384c # via ppm-utils furl==2.1.3 \ --hash=sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e \ @@ -254,6 +260,35 @@ furl==2.1.3 \ # via # django-dbmi-client # ppm-utils +google-api-core==2.18.0 \ + --hash=sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6 \ + --hash=sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9 + # via google-api-python-client +google-api-python-client==2.123.0 \ + --hash=sha256:1c2bcaa846acf5bac4d6f244d8373d4de9de73d64eb6e77b56767ab4cf681419 \ + --hash=sha256:a17226b02f71de581afe045437b441844110a9cd91580b73549d41108cf1b9f0 + # via ppm-utils +google-auth==2.29.0 \ + --hash=sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360 \ + --hash=sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415 + # via + # google-api-core + # google-api-python-client + # google-auth-httplib2 +google-auth-httplib2==0.2.0 \ + --hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \ + --hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d + # via google-api-python-client +googleapis-common-protos==1.63.0 \ + --hash=sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e \ + --hash=sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632 + # via google-api-core +httplib2==0.22.0 \ + --hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \ + --hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81 + # via + # google-api-python-client + # google-auth-httplib2 idna==3.6 \ --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f @@ -272,10 +307,39 @@ orderedmultidict==1.0.1 \ --hash=sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad \ --hash=sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3 # via furl -ppm-utils==0.15.3 \ - --hash=sha256:3b35313c90a39deb007949c7e1d1d1f9defc2d3da94b7be66af9db0e3feafc2f \ - --hash=sha256:9dd11da784ee202b36d00f114726bdcfab69e4ed612594c35ce4051fda3f3eb5 +ppm-utils==2.0.0 \ + --hash=sha256:81e8c29cda46827fa40581e12d49a66cac3b28ff63ec2de9a95d06a6622dff63 # via -r requirements.in +proto-plus==1.23.0 \ + --hash=sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2 \ + --hash=sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c + # via google-api-core +protobuf==4.25.3 \ + --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ + --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ + --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ + --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ + --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ + --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ + --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ + --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ + --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ + --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ + --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 + # via + # google-api-core + # googleapis-common-protos + # proto-plus +pyasn1==0.5.1 \ + --hash=sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58 \ + --hash=sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.3.0 \ + --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ + --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d + # via google-auth pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 @@ -284,6 +348,10 @@ pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via django-dbmi-client +pyparsing==3.1.2 \ + --hash=sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad \ + --hash=sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742 + # via httplib2 python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 @@ -309,7 +377,17 @@ requests==2.31.0 \ # -r requirements.in # django-dbmi-client # fhirclient + # google-api-core # ppm-utils + # requests-auth-aws-sigv4 +requests-auth-aws-sigv4==0.7 \ + --hash=sha256:1f6c7f63a0696a8f131a2ff21a544380f43c11f54d72600f6f2a1d402bd41d41 \ + --hash=sha256:3d2a475cccbf85d4c93b8bd052d072e5c3f8e77022fd621b69a5b11ac2c139c8 + # via ppm-utils +rsa==4.9 \ + --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ + --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 + # via google-auth s3transfer==0.10.1 \ --hash=sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19 \ --hash=sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d @@ -326,6 +404,14 @@ sqlparse==0.4.4 \ --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c # via django +typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via ppm-utils +uritemplate==4.1.1 \ + --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ + --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e + # via google-api-python-client urllib3==2.2.1 \ --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 From b567310bd1d265e65a4fdd4294c741dd0fd95a3c Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 28 Jun 2024 12:48:55 -0600 Subject: [PATCH 3/4] PPM-766: Dependency updates --- requirements.in | 2 +- requirements.txt | 223 ++++++++++++++++++++++++----------------------- 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/requirements.in b/requirements.in index b24bda1..9c04bd3 100644 --- a/requirements.in +++ b/requirements.in @@ -6,6 +6,6 @@ django-dbmi-client<2.0 django-health-check<4.0 django-ses<4.0 djangorestframework<4.0 -ppm-utils<3.0 +ppm-utils<3 raven<7.0 requests<3.0 diff --git a/requirements.txt b/requirements.txt index 26fe2b1..63c9631 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in @@ -8,15 +8,15 @@ asgiref==3.8.1 \ --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \ --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590 # via django -boto3==1.34.69 \ - --hash=sha256:2e25ef6bd325217c2da329829478be063155897d8d3b29f31f7f23ab548519b1 \ - --hash=sha256:898a5fed26b1351352703421d1a8b886ef2a74be6c97d5ecc92432ae01fda203 +boto3==1.34.135 \ + --hash=sha256:344f635233c85dbb509b87638232ff9132739f90bb5e6bf01fa0e0a521a9107e \ + --hash=sha256:6f5d7a20afbe45e3f7c6b5e96071752d36c3942535b1f7924964f1fdf25376a7 # via # django-ses # ppm-utils -botocore==1.34.69 \ - --hash=sha256:d1ab2bff3c2fd51719c2021d9fa2f30fbb9ed0a308f69e9a774ac92c8091380a \ - --hash=sha256:d3802d076d4d507bf506f9845a6970ce43adc3d819dd57c2791f5c19ed6e5950 +botocore==1.34.135 \ + --hash=sha256:2e72f37072f75cb1391fca9d7a4c32cecb52a3557d62431d0f59d5311dc7d0cf \ + --hash=sha256:3aa9e85e7c479babefb5a590e844435449df418085f3c74d604277bc52dc3109 # via # boto3 # s3transfer @@ -24,9 +24,9 @@ cachetools==5.3.3 \ --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 # via google-auth -certifi==2024.2.2 \ - --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ - --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 +certifi==2024.6.2 \ + --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ + --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 # via requests cffi==1.16.0 \ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ @@ -174,43 +174,43 @@ charset-normalizer==3.3.2 \ --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 # via requests -cryptography==42.0.5 \ - --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ - --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ - --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ - --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ - --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ - --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ - --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ - --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ - --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ - --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ - --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ - --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ - --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ - --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ - --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ - --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ - --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ - --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ - --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ - --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ - --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ - --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ - --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ - --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ - --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ - --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ - --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ - --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ - --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ - --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ - --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ - --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 +cryptography==42.0.8 \ + --hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \ + --hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \ + --hash=sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b \ + --hash=sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c \ + --hash=sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1 \ + --hash=sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648 \ + --hash=sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949 \ + --hash=sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba \ + --hash=sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c \ + --hash=sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9 \ + --hash=sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d \ + --hash=sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c \ + --hash=sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e \ + --hash=sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2 \ + --hash=sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d \ + --hash=sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7 \ + --hash=sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70 \ + --hash=sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2 \ + --hash=sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7 \ + --hash=sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14 \ + --hash=sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe \ + --hash=sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e \ + --hash=sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71 \ + --hash=sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961 \ + --hash=sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7 \ + --hash=sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c \ + --hash=sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28 \ + --hash=sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842 \ + --hash=sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902 \ + --hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \ + --hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \ + --hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e # via django-dbmi-client -django==4.2.11 \ - --hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \ - --hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3 +django==4.2.13 \ + --hash=sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5 \ + --hash=sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -236,17 +236,17 @@ django-crispy-forms==1.14.0 \ django-dbmi-client==1.0.8 \ --hash=sha256:34d71b69387b33afd2ae238b441dcc158e02f5c3bab43632002b789f8deb8030 # via -r requirements.in -django-health-check==3.18.1 \ - --hash=sha256:2c89a326cd79830e2fc6808823a9e7e874ab23f7aef3ff2c4d1194c998e1dca1 \ - --hash=sha256:44552d55ae8950c9548d3b90f9d9fd5570b57446a19b2a8e674c82f993cb7a2c +django-health-check==3.18.3 \ + --hash=sha256:18b75daca4551c69a43f804f9e41e23f5f5fb9efd06cf6a313b3d5031bb87bd0 \ + --hash=sha256:f5f58762b80bdf7b12fad724761993d6e83540f97e2c95c42978f187e452fa07 # via -r requirements.in -django-ses==3.5.2 \ - --hash=sha256:90c68cc6ca3467893faa8499981c81ba8ff2bd3f3acb08c06423a4142d6a0fc6 \ - --hash=sha256:b6d94689bc15de02a11e84f05a5bf4a7895688e570c6f07c21698094debc6ced +django-ses==3.6.0 \ + --hash=sha256:ea08bea9e1aab71f9fbf43b30733a27eff76cea3797b7ebeab9f6bc5d3df6b37 \ + --hash=sha256:f3f69b97444fdbda41946c7349c63e1a0ea8284d9e9acd6f4b5cb3dba5030829 # via -r requirements.in -djangorestframework==3.15.1 \ - --hash=sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6 \ - --hash=sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1 +djangorestframework==3.15.2 \ + --hash=sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20 \ + --hash=sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad # via # -r requirements.in # django-dbmi-client @@ -260,17 +260,17 @@ furl==2.1.3 \ # via # django-dbmi-client # ppm-utils -google-api-core==2.18.0 \ - --hash=sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6 \ - --hash=sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9 +google-api-core==2.19.1 \ + --hash=sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125 \ + --hash=sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd # via google-api-python-client -google-api-python-client==2.123.0 \ - --hash=sha256:1c2bcaa846acf5bac4d6f244d8373d4de9de73d64eb6e77b56767ab4cf681419 \ - --hash=sha256:a17226b02f71de581afe045437b441844110a9cd91580b73549d41108cf1b9f0 +google-api-python-client==2.135.0 \ + --hash=sha256:91742fa4c779d48456c0256ef346fa1cc185ba427176d3277e35141fa3268026 \ + --hash=sha256:b552a28123ed95493035698db80e8ed78c9106a8b422e63a175150b9b55b704e # via ppm-utils -google-auth==2.29.0 \ - --hash=sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360 \ - --hash=sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415 +google-auth==2.30.0 \ + --hash=sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5 \ + --hash=sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688 # via # google-api-core # google-api-python-client @@ -279,9 +279,9 @@ google-auth-httplib2==0.2.0 \ --hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \ --hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d # via google-api-python-client -googleapis-common-protos==1.63.0 \ - --hash=sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e \ - --hash=sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632 +googleapis-common-protos==1.63.2 \ + --hash=sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945 \ + --hash=sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87 # via google-api-core httplib2==0.22.0 \ --hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \ @@ -289,9 +289,9 @@ httplib2==0.22.0 \ # via # google-api-python-client # google-auth-httplib2 -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests isodate==0.6.1 \ --hash=sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96 \ @@ -307,42 +307,43 @@ orderedmultidict==1.0.1 \ --hash=sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad \ --hash=sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3 # via furl -ppm-utils==2.0.0 \ - --hash=sha256:81e8c29cda46827fa40581e12d49a66cac3b28ff63ec2de9a95d06a6622dff63 +ppm-utils==2.0.1 \ + --hash=sha256:e5772b7a2a99600c3291afa3e6dfc883953956bc0f1a10111a1cc77111c4cc82 \ + --hash=sha256:fa011cd8c8b6cc031d6456791c8162d40bb8790c25d8f448cceaf31281f43420 # via -r requirements.in -proto-plus==1.23.0 \ - --hash=sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2 \ - --hash=sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c +proto-plus==1.24.0 \ + --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ + --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 # via google-api-core -protobuf==4.25.3 \ - --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ - --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ - --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ - --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ - --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ - --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ - --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ - --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ - --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ - --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ - --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 +protobuf==5.27.2 \ + --hash=sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505 \ + --hash=sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b \ + --hash=sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38 \ + --hash=sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863 \ + --hash=sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470 \ + --hash=sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6 \ + --hash=sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce \ + --hash=sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca \ + --hash=sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5 \ + --hash=sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e \ + --hash=sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714 # via # google-api-core # googleapis-common-protos # proto-plus -pyasn1==0.5.1 \ - --hash=sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58 \ - --hash=sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c +pyasn1==0.6.0 \ + --hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \ + --hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473 # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 \ - --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ - --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d +pyasn1-modules==0.4.0 \ + --hash=sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6 \ + --hash=sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b # via google-auth -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ @@ -370,9 +371,9 @@ raven==6.10.0 \ # via # -r requirements.in # django-dbmi-client -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # -r requirements.in # django-dbmi-client @@ -388,9 +389,9 @@ rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 # via google-auth -s3transfer==0.10.1 \ - --hash=sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19 \ - --hash=sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d +s3transfer==0.10.2 \ + --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ + --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 # via boto3 six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ @@ -400,21 +401,21 @@ six==1.16.0 \ # isodate # orderedmultidict # python-dateutil -sqlparse==0.4.4 \ - --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ - --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c +sqlparse==0.5.0 \ + --hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \ + --hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663 # via django -typing-extensions==4.10.0 \ - --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ - --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via ppm-utils uritemplate==4.1.1 \ --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e # via google-api-python-client -urllib3==2.2.1 \ - --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ - --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 +urllib3==2.2.2 \ + --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ + --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 # via # botocore # requests From 6adda620649b3053f3cee5ee6d4124af5364794b Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 1 Jul 2024 05:20:33 -0600 Subject: [PATCH 4/4] PPM-766 - Dependencies update --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 63c9631..16e65d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in @@ -8,15 +8,15 @@ asgiref==3.8.1 \ --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \ --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590 # via django -boto3==1.34.135 \ - --hash=sha256:344f635233c85dbb509b87638232ff9132739f90bb5e6bf01fa0e0a521a9107e \ - --hash=sha256:6f5d7a20afbe45e3f7c6b5e96071752d36c3942535b1f7924964f1fdf25376a7 +boto3==1.34.136 \ + --hash=sha256:0314e6598f59ee0f34eb4e6d1a0f69fa65c146d2b88a6e837a527a9956ec2731 \ + --hash=sha256:d41037e2c680ab8d6c61a0a4ee6bf1fdd9e857f43996672830a95d62d6f6fa79 # via # django-ses # ppm-utils -botocore==1.34.135 \ - --hash=sha256:2e72f37072f75cb1391fca9d7a4c32cecb52a3557d62431d0f59d5311dc7d0cf \ - --hash=sha256:3aa9e85e7c479babefb5a590e844435449df418085f3c74d604277bc52dc3109 +botocore==1.34.136 \ + --hash=sha256:7f7135178692b39143c8f152a618d2a3b71065a317569a7102d2306d4946f42f \ + --hash=sha256:c63fe9032091fb9e9477706a3ebfa4d0c109b807907051d892ed574f9b573e61 # via # boto3 # s3transfer