Repository: Clinical-Genomics/patientMatcher

Issue:
Add option to notify minimal or complete matching info by emails
For security reasons it's good to introduce an option to notify complete or partial info for MME matches by email.

I've changed the email body to follow the MME directives, see #65. Still it's good to have the option of NOT showing variants and phenotype terms of the matchings. 

So add a config parameter that allows to show only contact info and cases ID in the notification emails.

Discussion:

Patch:
diff --git a/README.md b/README.md
index b2eed1c..bf20b6d 100644
--- a/README.md
+++ b/README.md
@@ -315,6 +315,8 @@ in the configuration file (config.py). If you want to test your email configurat
 pmatcher test email -recipient your_email@email.com
 ```
 
+It is possible to choose to send complete or partial info for matching patients in the email notification body. Set **NOTIFY_COMPLETE** to True (in config.py) if you want to notify complete patient data (i.e. variants and phenotypes will be shared by email) or set it to False if email body should contain ONLY contact info and patient IDs for all matching patients. This latter option is useful to better secure the access to sensitive data.<br>
+
 Once these parameters are set to valid values a notification email will be sent in the following cases:
 
  - A patient is added to the database and the add request triggers a search on external nodes. There is at least one returned result (/patient/add endpoint).
@@ -326,6 +328,5 @@ Once these parameters are set to valid values a notification email will be sent
 
 
 
-
 [travis-url]: https://travis-ci.org/Clinical-Genomics/patientMatcher
 [travis-image]: https://img.shields.io/travis/Clinical-Genomics/patientMatcher.svg?style=flat-square
diff --git a/instance/config.py b/instance/config.py
index c4a3746..03b2e42 100644
--- a/instance/config.py
+++ b/instance/config.py
@@ -24,8 +24,12 @@ MME_HOST = 'https://www.scilifelab.se/facilities/clinical-genomics-stockholm'
 
 # Email notification params.
 # Required only if you want to send match notifications to patients contacts
-#MAIL_SERVER = smtp_server
-#MAIL_PORT = mail_port
+#MAIL_SERVER = mail_port
+#MAIL_PORT = email_port
 #MAIL_USE_SSL = True or False
-#MAIL_USERNAME = mail_username
-#MAIL_PASSWORD = mail_password
+#MAIL_USERNAME = 'user_email@mail.se'
+#MAIL_PASSWORD = 'mail_password'
+
+# Set NOTIFY_COMPLETE to False if you don't want to notify variants and phenotypes by email
+# This way only contact info and matching patients ID will be notified in email body
+#NOTIFY_COMPLETE = True
diff --git a/patientMatcher/server/views.py b/patientMatcher/server/views.py
index b5e96a1..aed767b 100644
--- a/patientMatcher/server/views.py
+++ b/patientMatcher/server/views.py
@@ -45,7 +45,8 @@ def add():
     # and match notifications are on
     if current_app.config.get('MAIL_SERVER') and matching_obj and len(matching_obj.get('results')):
         # send an email to patient's contact:
-        notify_match_external(match_obj=matching_obj, admin_email=current_app.config.get('MAIL_USERNAME'), mail=current_app.mail)
+        notify_match_external(match_obj=matching_obj, admin_email=current_app.config.get('MAIL_USERNAME'),
+            mail=current_app.mail, notify_complete=current_app.config.get('NOTIFY_COMPLETE'))
 
     resp = jsonify(message)
     resp.status_code = 200
@@ -189,7 +190,8 @@ def match_external(patient_id):
     # and match notifications are on
     if current_app.config.get('MAIL_SERVER') and matching_obj and len(results):
         # send an email to patient's contact:
-        notify_match_external(match_obj=matching_obj, admin_email=current_app.config.get('MAIL_USERNAME'), mail=current_app.mail)
+        notify_match_external(match_obj=matching_obj, admin_email=current_app.config.get('MAIL_USERNAME'),
+            mail=current_app.mail, notify_complete=current_app.config.get('NOTIFY_COMPLETE'))
 
     resp = jsonify({'results':results})
     resp.status_code = 200
@@ -223,7 +225,8 @@ def match_internal():
     # if notifications are on and there are matching results
     if current_app.config.get('MAIL_SERVER') and len(matches):
         notify_match_internal(database=current_app.db, match_obj=match_obj,
-            admin_email=current_app.config.get('MAIL_USERNAME'), mail=current_app.mail)
+            admin_email=current_app.config.get('MAIL_USERNAME'), mail=current_app.mail,
+            notify_complete=current_app.config.get('NOTIFY_COMPLETE'))
 
 
     validate_response = controllers.validate_response({'results': matches})
diff --git a/patientMatcher/utils/notify.py b/patientMatcher/utils/notify.py
index 6d18c50..1ddb27b 100644
--- a/patientMatcher/utils/notify.py
+++ b/patientMatcher/utils/notify.py
@@ -5,7 +5,7 @@ from flask_mail import Message
 LOG = logging.getLogger(__name__)
 
 
-def notify_match_internal(database, match_obj, admin_email, mail):
+def notify_match_internal(database, match_obj, admin_email, mail, notify_complete):
     """Send an email to patient contacts after an internal match
 
     Args:
@@ -13,11 +13,11 @@ def notify_match_internal(database, match_obj, admin_email, mail):
         match_obj(dict): an object containing both query patient(dict) and matching results(list)
         admin_email(str): email of the server admin
         mail(flask_mail.Mail): an email instance
+        notify_complete(bool): set to False to NOT notify variants and phenotype terms by email
     """
     # Internal matching can be triggered by a patient in the same database or by a patient on a connected node.
     # In the first case notify both querier contact and contacts in the result patients.
     # in the second case notify only contacts from patients in the results list.
-
     sender = admin_email
     patient_id = None
     patient_label = None
@@ -26,14 +26,15 @@ def notify_match_internal(database, match_obj, admin_email, mail):
     email_subject = 'MatchMaker Exchange: new patient match available.'
     email_body = None
 
-    # check if query patient belongs to patientMatcher database:
+    # check if query patient already belongs to patientMatcher database:
     internal_patient = database['patients'].find_one({'_id':match_obj['data']['patient']['id']})
     if internal_patient:
-        #If patient used for the search is on patientMatcher database, notify querier as well:
+        #If patient used for the search is in patientMatcher database, notify querier as well:
         patient_id = match_obj['data']['patient']['id']
         patient_label = match_obj['data']['patient'].get('label')
         recipient = match_obj['data']['patient']['contact']['href'][7:]
-        email_body = active_match_email_body(patient_id=patient_id, match_results=match_obj['results'], patient_label=patient_label, external_match=False)
+        email_body = active_match_email_body(patient_id=patient_id, match_results=match_obj['results'], patient_label=patient_label,
+            external_match=False, notify_complete=notify_complete)
         LOG.info('Sending an internal match notification for query patient with ID:{0}. Patient contact: {1}'.format(patient_id, recipient))
 
         kwargs = dict(subject=email_subject, html=email_body, sender=sender, recipients=[recipient])
@@ -45,7 +46,6 @@ def notify_match_internal(database, match_obj, admin_email, mail):
             LOG.error('An error occurred while sending an internal match notification: {}'.format(err))
 
     # Loop over the result patients and notify their contact about the matching with query patient
-
     for result in match_obj['results'][0]['patients']: #this list has only one element since there is only one internal node
 
         patient_id = result['patient']['id']
@@ -56,7 +56,7 @@ def notify_match_internal(database, match_obj, admin_email, mail):
 
         patient_label =  result['patient'].get('label')
         recipient = result['patient']['contact']['href'][7:]
-        email_body = passive_match_email_body(patient_id, match_obj['data']['patient'], patient_label)
+        email_body = passive_match_email_body(patient_id, match_obj['data']['patient'], patient_label, notify_complete)
         LOG.info('Sending an internal match notification for match result with ID {}'.format(patient_id))
 
         kwargs = dict(subject=email_subject, html=email_body, sender=sender, recipients=[recipient])
@@ -68,21 +68,22 @@ def notify_match_internal(database, match_obj, admin_email, mail):
             LOG.error('An error occurred while sending an internal match notification: {}'.format(err))
 
 
-def notify_match_external(match_obj, admin_email, mail):
+def notify_match_external(match_obj, admin_email, mail, notify_complete):
     """Send an email to patients contacts to notify a match on external nodes
 
     Args:
         match_obj(dict): an object containing both query patient(dict) and matching results(list)
         admin_email(str): email of the server admin
         mail(flask_mail.Mail): an email instance
+        notify_complete(bool): set to False to NOT notify variants and phenotype terms by email
     """
-
     sender = admin_email
     patient_id = match_obj['data']['patient']['id']
     patient_label = match_obj['data']['patient'].get('label')
     recipient = match_obj['data']['patient']['contact']['href'][7:]
     email_subject = 'MatchMaker Exchange: new patient match available.'
-    email_body = active_match_email_body(patient_id=patient_id, match_results=match_obj['results'], patient_label=patient_label, external_match=True)
+    email_body = active_match_email_body(patient_id=patient_id, match_results=match_obj['results'], patient_label=patient_label,
+        external_match=True, notify_complete=notify_complete)
     LOG.info('Sending an external match notification for query patient with ID {0}. Patient contact: {1}'.format(patient_id, recipient))
 
     kwargs = dict(subject=email_subject, html=email_body, sender=sender, recipients=[recipient])
@@ -94,7 +95,7 @@ def notify_match_external(match_obj, admin_email, mail):
         LOG.error('An error occurred while sending an external match notification: {}'.format(err))
 
 
-def active_match_email_body(patient_id, match_results, patient_label=None, external_match=False):
+def active_match_email_body(patient_id, match_results, patient_label=None, external_match=False, notify_complete=False):
     """Returns the body message of the notification email when the patient was used as query patient
 
     Args:
@@ -102,7 +103,7 @@ def active_match_email_body(patient_id, match_results, patient_label=None, exter
         match_results(list): a list of patients which match with the patient whose contact is going to be notified
         external_match(bool): True == match in connected nodes, False == match with other patients in database
         patient_label(str): the label of the patient submitted by the  MME user which will be notified (not mandatory field)
-
+        notify_complete(bool): set to False to NOT notify variants and phenotype terms by email
 
     Returns:
         html(str): the body message
@@ -116,62 +117,65 @@ def active_match_email_body(patient_id, match_results, patient_label=None, exter
         <strong>MatchMaker Exchange patient matching notification:</strong><br><br>
         Patient with ID <strong>{0}</strong>, label <strong>{1}</strong>.
         This search returned these potential matches</strong>:<br>
-        <strong>{2}</strong>
+        <strong>{2}</strong><br>
         You might directly contact the matching part using the address specified in patient's data or review matching
         results in the portal you used to submit your patient.
         <br><br>
         Kind regards,<br>
         The PatientMatcher team
-    """.format(patient_id, patient_label, html_format(match_results))
+    """.format(patient_id, patient_label, html_format(match_results, 0, notify_complete))
 
     return html
 
 
-def passive_match_email_body(patient_id, matched_patient, patient_label=None,):
+def passive_match_email_body(patient_id, matched_patient, patient_label=None, notify_complete=False):
     """Returns the body message of the notification email when the patient was used as query patient
 
     Args:
         patient_id(str): the ID of the patient submitted by the MME user which will be notified
         matched_patient(dict): a patient object
         patient_label(str): the label of the patient submitted by the  MME user which will be notified (not mandatory field)
+        notify_complete(bool): set to False to NOT notify variants and phenotype terms by email
 
     Returns:
         html(str): the body message
     """
-
     html = """
         ***This is an automated message, please do not reply.***<br>
         <strong>MatchMaker Exchange patient matching notification:</strong><br><br>
         Patient with <strong>ID {0}</strong>,<strong> label {1}</strong> was recently returned as a match result
-        in a search performed using a patient with these charateristics:<br>
+        in a search performed using a patient with these specifications:<br>
         <strong>{2}</strong><br>
         You might directly contact the matching part using the address specified in patient's data or review matching
         results in the portal you used to submit your patient.
         <br><br>
         Kind regards,<br>
         The PatientMatcher team
-    """.format(patient_id, patient_label, html_format(matched_patient))
+    """.format(patient_id, patient_label, html_format(matched_patient, 0, notify_complete))
 
     return html
 
 
-def html_format(obj, indent=0):
+def html_format(obj, indent=0, notify_complete=False):
     """Formats one or more patient objects to a nice html string
 
     Args:
         obj(list): a list of patient objects or a patient object
+        notify_complete(bool): set to False to NOT notify variants and phenotype terms by email
     """
-    if isinstance(obj, list):
+    if isinstance(obj, list): # a list pf match results
         htmls = []
         for k in obj:
-            htmls.append(html_format(k,indent+1))
+            htmls.append(html_format(obj=k, indent=indent+1, notify_complete=notify_complete))
 
         return '[<div style="margin-left: %dem">%s</div>]' % (indent, ',<br>'.join(htmls))
 
-    if isinstance(obj, dict):
+    if isinstance(obj, dict): # patient object
         htmls = []
         for k,v in obj.items():
-            htmls.append("<span style='font-style: italic; color: #888'>%s</span>: %s" % (k,html_format(v,indent+1)))
+            if notify_complete or k in ['node', 'patients', 'patient', 'contact', 'id', 'name', 'href', 'institution']:
+                htmls.append("<span style='font-style: italic; color: #888'>%s</span>: %s" % (k,html_format(obj=v,indent=indent+1,
+                    notify_complete=notify_complete)))
 
         return '{<div style="margin-left: %dem">%s</div>}' % (indent, ',<br>'.join(htmls))
 


Test patch:
diff --git a/tests/utils/test_notify.py b/tests/utils/test_notify.py
index 0791cdc..32c1a88 100644
--- a/tests/utils/test_notify.py
+++ b/tests/utils/test_notify.py
@@ -8,7 +8,8 @@ def test_notify_match_external(match_objs, mock_sender, mock_mail):
     assert match_obj['match_type'] == 'external'
 
     # When calling the function that sends external match notifications
-    notify_match_external(match_obj, mock_sender, mock_mail)
+    notify_complete = True # test notification of complete patient data by email
+    notify_match_external(match_obj, mock_sender, mock_mail, notify_complete)
 
     # make sure send method was called
     assert mock_mail._send_was_called
@@ -28,7 +29,8 @@ def test_notify_match_internal(database, match_objs, mock_sender, mock_mail):
     assert database['patients'].find().count() == 1
 
     # When calling the function that sends internal match notifications
-    notify_match_internal(database, match_obj, mock_sender, mock_mail)
+    notify_complete = False # test notification of partial patient data by email
+    notify_match_internal(database, match_obj, mock_sender, mock_mail, notify_complete)
 
     # Test the function that formats the matching results to HTML:
     formatted_results = html_format(match_obj['results'])