changeset 3215:3c8aa357f08d

Task #15167: Update existing patients and studies on sync; Reformat code
author Chris Zimmerman <chris@teffalump.com>
date Sun, 17 Feb 2019 23:33:05 -0800
parents 37dd03f759f8
children e964f822c8fd
files tryton/health_orthanc/__init__.py tryton/health_orthanc/health_orthanc.py tryton/health_orthanc/wizard/wizard.py
diffstat 3 files changed, 272 insertions(+), 154 deletions(-) [+]
line wrap: on
line diff
--- a/tryton/health_orthanc/__init__.py
+++ b/tryton/health_orthanc/__init__.py
@@ -23,6 +23,7 @@
 from .health_orthanc import *
 from .wizard import *
 
+
 def register():
     Pool.register(
         AddOrthancInit,
@@ -31,7 +32,7 @@
         OrthancStudy,
         OrthancPatient,
         Orthanc,
-        module='health_orthanc', type_='model')
-    Pool.register(
-        FullSyncOrthanc,
-        module='health_orthanc', type_='wizard')
+        module="health_orthanc",
+        type_="model",
+    )
+    Pool.register(FullSyncOrthanc, module="health_orthanc", type_="wizard")
--- a/tryton/health_orthanc/health_orthanc.py
+++ b/tryton/health_orthanc/health_orthanc.py
@@ -22,44 +22,59 @@
 from trytond.pool import Pool
 from trytond.transaction import Transaction
 from orthanc_rest_client import Orthanc as RestClient
-import pendulum
 from requests.auth import HTTPBasicAuth as auth
 from requests.exceptions import HTTPError, ConnectionError
 from datetime import datetime
 import logging
+import pendulum
 
-__all__=['OrthancServerConfig', 'OrthancPatient', 'OrthancStudy', 'Orthanc']
+__all__ = ["OrthancServerConfig", "OrthancPatient", "OrthancStudy", "Orthanc"]
 
 logger = logging.getLogger(__name__)
 
+
 class OrthancServerConfig(ModelSQL, ModelView):
-    '''Orthanc server details'''
+    """Orthanc server details"""
 
-    __name__ = 'gnuhealth.orthanc.config'
-    _rec_name = 'label'
+    __name__ = "gnuhealth.orthanc.config"
+    _rec_name = "label"
 
-    label = fields.Char('Label', required=True, help="Label for server (eg., remote1)")
-    domain = fields.Char('URL', required=True, help="The full URL of the Orthanc server")
-    user = fields.Char('Username', required=True, help="Username for Orthanc REST server")
-    password = fields.Char('Password', required=True, help="Password for Orthanc REST server")
-    last = fields.BigInteger('Last Index', readonly=True, help="Index of last change")
-    sync_time = fields.DateTime('Sync Time', readonly=True, help="Time of last server sync")
-    validated = fields.Boolean('Validated', help="The server details have been successfully checked")
-    since_sync = fields.Function(fields.TimeDelta('Since last sync', help="Time since last sync"), 'get_since_sync')
-    since_sync_readable = fields.Function(fields.Char('Since last sync', help="Time since last sync"), 'get_since_sync_readable')
-    patients = fields.One2Many('gnuhealth.orthanc.patient', 'server', 'Patients')
-    studies = fields.One2Many('gnuhealth.orthanc.study', 'server', 'Studies')
+    label = fields.Char("Label", required=True, help="Label for server (eg., remote1)")
+    domain = fields.Char(
+        "URL", required=True, help="The full URL of the Orthanc server"
+    )
+    user = fields.Char(
+        "Username", required=True, help="Username for Orthanc REST server"
+    )
+    password = fields.Char(
+        "Password", required=True, help="Password for Orthanc REST server"
+    )
+    last = fields.BigInteger("Last Index", readonly=True, help="Index of last change")
+    sync_time = fields.DateTime(
+        "Sync Time", readonly=True, help="Time of last server sync"
+    )
+    validated = fields.Boolean(
+        "Validated", help="Whether the server details have been successfully checked"
+    )
+    since_sync = fields.Function(
+        fields.TimeDelta("Since last sync", help="Time since last sync"),
+        "get_since_sync",
+    )
+    since_sync_readable = fields.Function(
+        fields.Char("Since last sync", help="Time since last sync"),
+        "get_since_sync_readable",
+    )
+    patients = fields.One2Many("gnuhealth.orthanc.patient", "server", "Patients")
+    studies = fields.One2Many("gnuhealth.orthanc.study", "server", "Studies")
 
     @classmethod
     def __setup__(cls):
-       super(OrthancServerConfig, cls).__setup__()
-       t = cls.__table__()
-       cls._sql_constraints = [
-               ('label_unique', Unique(t, t.label), 'The label must be unique.'),
-               ]
-       cls._buttons.update({
-              'do_sync': {},
-              })
+        super(OrthancServerConfig, cls).__setup__()
+        t = cls.__table__()
+        cls._sql_constraints = [
+            ("label_unique", Unique(t, t.label), "The label must be unique.")
+        ]
+        cls._buttons.update({"do_sync": {}})
 
     @classmethod
     @ModelView.button
@@ -71,54 +86,71 @@
         """Sync from changes endpoint"""
 
         pool = Pool()
-        patient = pool.get('gnuhealth.orthanc.patient')
-        study = pool.get('gnuhealth.orthanc.study')
+        patient = pool.get("gnuhealth.orthanc.patient")
+        study = pool.get("gnuhealth.orthanc.study")
 
         if not servers:
-            servers = cls.search([('domain', '!=', None),
-                                    ('validated', '=', True)])
+            servers = cls.search([("domain", "!=", None), ("validated", "=", True)])
 
-        logger.info('Starting sync')
+        logger.info("Starting sync")
         for server in servers:
-            if not server.validated: continue
-            logger.info('Getting new changes for <{}>'.format(server.label))
+            if not server.validated:
+                continue
+            logger.info("Getting new changes for <{}>".format(server.label))
             orthanc = RestClient(server.domain, auth=auth(server.user, server.password))
             curr = server.last
-            new_studies = []
-            update_studies = []
-            new_patients = []
-            update_patients = []
+            new_patients = set()
+            update_patients = set()
+            new_studies = set()
+            update_studies = set()
+
             while True:
                 try:
                     changes = orthanc.get_changes(since=curr)
                 except:
                     server.validated = False
-                    logger.info('Invalid details for <{}>'.format(server.label))
+                    logger.info("Invalid details for <{}>".format(server.label))
                     break
-                for change in changes['Changes']:
-                    type_ = change['ChangeType']
-                    if type_ == 'NewStudy':
-                        new_studies.append(orthanc.get_study(change['ID']))
-                    elif type_ == 'StableStudy':
-                        update_studies.append(orthanc.get_study(change['ID']))
-                    elif type_ == 'NewPatient':
-                        new_patients.append(orthanc.get_patient(change['ID']))
-                    elif type_ == 'StablePatient':
-                        update_patients.append(orthanc.get_patient(change['ID']))
+                for change in changes["Changes"]:
+                    type_ = change["ChangeType"]
+                    if type_ == "NewStudy":
+                        new_studies.add(change["ID"])
+                    elif type_ == "StableStudy":
+                        update_studies.add(change["ID"])
+                    elif type_ == "NewPatient":
+                        new_patients.add(change["ID"])
+                    elif type_ == "StablePatient":
+                        update_patients.add(change["ID"])
                     else:
                         pass
-                curr = changes['Last']
-                if changes['Done'] == True:
-                    logger.info('<{}> at newest change'.format(server.label))
+                curr = changes["Last"]
+                if changes["Done"] == True:
+                    logger.info("<{}> at newest change".format(server.label))
                     break
-            patient.create_patients(new_patients, server)
-            study.create_studies(new_studies, server)
+
+            update_patients -= new_patients
+            update_studies -= new_studies
+            patient.create_patients(
+                [orthanc.get_patient(p) for p in new_patients], server
+            )
+            patient.update_patients(
+                [orthanc.get_patient(p) for p in update_patients], server
+            )
+            study.create_studies([orthanc.get_study(s) for s in new_studies], server)
+            study.update_studies([orthanc.get_study(s) for s in update_studies], server)
             server.last = curr
             server.sync_time = datetime.now()
-            logger.info('{} sync complete: {} new patients, {} new studies'.format(server.label, len(new_patients), len(new_studies)))
+            logger.info(
+                "<{}> sync complete: {} new patients, {} update patients, {} new studies, {} updated studies".format(
+                    server.label,
+                    len(new_patients),
+                    len(update_patients),
+                    len(new_studies),
+                    len(update_studies),
+                )
+            )
         cls.save(servers)
 
-
     @staticmethod
     def quick_check(domain, user, password):
         """Validate the server details"""
@@ -131,7 +163,7 @@
         else:
             return True
 
-    @fields.depends('domain', 'user', 'password')
+    @fields.depends("domain", "user", "password")
     def on_change_with_validated(self):
         return self.quick_check(self.domain, self.user, self.password)
 
@@ -143,20 +175,25 @@
             d = pendulum.now() - pendulum.instance(self.sync_time)
             return d.in_words(Transaction().language)
         except:
-            return ''
+            return ""
+
 
 class OrthancPatient(ModelSQL, ModelView):
-    '''Orthanc patient information'''
+    """Orthanc patient information"""
 
-    __name__ = 'gnuhealth.orthanc.patient'
+    __name__ = "gnuhealth.orthanc.patient"
 
-    patient = fields.Many2One('gnuhealth.patient', 'Patient', help="Local linked patient")
-    name = fields.Char('PatientName', readonly=True)
-    bd = fields.Date('Birthdate', readonly=True)
-    ident = fields.Char('PatientID', readonly=True)
-    uuid = fields.Char('PatientUUID', readonly=True, required=True)
-    studies = fields.One2Many('gnuhealth.orthanc.study', 'patient', 'Studies', readonly=True)
-    server = fields.Many2One('gnuhealth.orthanc.config', 'Server', readonly=True)
+    patient = fields.Many2One(
+        "gnuhealth.patient", "Patient", help="Local linked patient"
+    )
+    name = fields.Char("PatientName", readonly=True)
+    bd = fields.Date("Birthdate", readonly=True)
+    ident = fields.Char("PatientID", readonly=True)
+    uuid = fields.Char("PatientUUID", readonly=True, required=True)
+    studies = fields.One2Many(
+        "gnuhealth.orthanc.study", "patient", "Studies", readonly=True
+    )
+    server = fields.Many2One("gnuhealth.orthanc.config", "Server", readonly=True)
 
     @staticmethod
     def get_info_from_dicom(patients):
@@ -165,56 +202,80 @@
         data = []
         for patient in patients:
             try:
-                bd = datetime.strptime(patient['MainDicomTags']['PatientBirthDate'], '%Y%m%d').date()
+                bd = datetime.strptime(
+                    patient["MainDicomTags"]["PatientBirthDate"], "%Y%m%d"
+                ).date()
             except:
                 bd = None
-            data.append({
-                    'name': patient.get('MainDicomTags').get('PatientName'),
-                    'bd': bd,
-                    'ident': patient.get('MainDicomTags').get('PatientID'),
-                    'uuid': patient.get('ID'),
-                    })
+            data.append(
+                {
+                    "name": patient.get("MainDicomTags").get("PatientName"),
+                    "bd": bd,
+                    "ident": patient.get("MainDicomTags").get("PatientID"),
+                    "uuid": patient.get("ID"),
+                }
+            )
         return data
 
     @classmethod
     def update_patients(cls, patients, server):
-        pass
+        """Update patients"""
+
+        entries = cls.get_info_from_dicom(patients)
+        updates = []
+        for entry in entries:
+            try:
+                patient = cls.search(
+                    [("uuid", "=", entry["ident"]), ("server", "=", server)], limit=1
+                )[0]
+                patient.name = entry["name"]
+                patient.bd = entry["bd"]
+                patient.ident = entry["ident"]  # TODO Look for matching record
+                updates.append(patient)
+                logger.info("Updating {}".format(entry["ident"]))
+            except:
+                continue
+                logger.warning("Unable to update patient {}".format(entry["ident"]))
+        cls.save(updates)
 
     @classmethod
     def create_patients(cls, patients, server):
         """Create patients"""
 
         pool = Pool()
-        Patient = pool.get('gnuhealth.patient')
+        Patient = pool.get("gnuhealth.patient")
 
         entries = cls.get_info_from_dicom(patients)
         for entry in entries:
             try:
-                g_patient = Patient.search([('puid', '=', entry['ident'])], limit=1)[0]
-                logger.info('Matching PUID found for {}'.format(entry['uuid']))
+                g_patient = Patient.search([("puid", "=", entry["ident"])], limit=1)[0]
+                logger.info("Matching PUID found for {}".format(entry["uuid"]))
             except:
                 g_patient = None
-            entry['server'] = server
-            entry['patient'] = g_patient
+            entry["server"] = server
+            entry["patient"] = g_patient
         cls.create(entries)
 
+
 class OrthancStudy(ModelSQL, ModelView):
-    '''Orthanc study'''
+    """Orthanc study"""
 
-    __name__ = 'gnuhealth.orthanc.study'
+    __name__ = "gnuhealth.orthanc.study"
 
-    patient = fields.Many2One('gnuhealth.orthanc.patient', 'Patient', readonly=True)
-    uuid = fields.Char('UUID', readonly=True, required=True)
-    description = fields.Char('Description', readonly=True)
-    date = fields.Date('Date', readonly=True)
-    ident = fields.Char('ID', readonly=True)
-    institution = fields.Char('Institution', readonly=True, help="Imaging center where study was undertaken")
-    ref_phys = fields.Char('Referring Physician', readonly=True)
-    req_phys = fields.Char('Requesting Physician', readonly=True)
-    server = fields.Many2One('gnuhealth.orthanc.config', 'Server', readonly=True)
+    patient = fields.Many2One("gnuhealth.orthanc.patient", "Patient", readonly=True)
+    uuid = fields.Char("UUID", readonly=True, required=True)
+    description = fields.Char("Description", readonly=True)
+    date = fields.Date("Date", readonly=True)
+    ident = fields.Char("ID", readonly=True)
+    institution = fields.Char(
+        "Institution", readonly=True, help="Imaging center where study was undertaken"
+    )
+    ref_phys = fields.Char("Referring Physician", readonly=True)
+    req_phys = fields.Char("Requesting Physician", readonly=True)
+    server = fields.Many2One("gnuhealth.orthanc.config", "Server", readonly=True)
 
     def get_rec_name(self, name):
-        return ': '.join((self.ident or self.uuid, self.description or ''))
+        return ": ".join((self.ident or self.uuid, self.description or ""))
 
     @staticmethod
     def get_info_from_dicom(studies):
@@ -224,52 +285,85 @@
 
         for study in studies:
             try:
-                date = datetime.strptime(study['MainDicomTags']['StudyDate'], '%Y%m%d').date()
+                date = datetime.strptime(
+                    study["MainDicomTags"]["StudyDate"], "%Y%m%d"
+                ).date()
             except:
                 date = None
             try:
-                description = study['MainDicomTags']['RequestedProcedureDescription']
+                description = study["MainDicomTags"]["RequestedProcedureDescription"]
             except:
                 description = None
-            data.append({
-                'parent_patient': study['ParentPatient'],
-                'uuid': study['ID'],
-                'description': description,
-                'date': date,
-                'ident': study.get('MainDicomTags').get('StudyID'),
-                'institution': study.get('MainDicomTags').get('InstitutionName'),
-                'ref_phys': study.get('MainDicomTags').get('ReferringPhysicianName'),
-                'req_phys': study.get('MainDicomTags').get('RequestingPhysician'),
-                })
+            data.append(
+                {
+                    "parent_patient": study["ParentPatient"],
+                    "uuid": study["ID"],
+                    "description": description,
+                    "date": date,
+                    "ident": study.get("MainDicomTags").get("StudyID"),
+                    "institution": study.get("MainDicomTags").get("InstitutionName"),
+                    "ref_phys": study.get("MainDicomTags").get(
+                        "ReferringPhysicianName"
+                    ),
+                    "req_phys": study.get("MainDicomTags").get("RequestingPhysician"),
+                }
+            )
         return data
 
     @classmethod
     def update_studies(cls, studies, server):
-        pass
+        """Update studies"""
+
+        entries = cls.get_info_from_dicom(studies)
+        updates = []
+        for entry in entries:
+            try:
+                study = cls.search(
+                    [("uuid", "=", entry["ident"]), ("server", "=", server)], limit=1
+                )[0]
+                study.description = entry["description"]
+                study.date = entry["date"]
+                study.ident = entry["ident"]
+                study.institution = entry["institution"]
+                study.ref_phys = entry["ref_phys"]
+                study.req_phys = entry["req_phys"]
+                updates.append(study)
+                logger.info("Updating {}".format(entry["ident"]))
+            except:
+                continue
+                logger.warning("Unable to update study {}".format(entry["ident"]))
+        cls.save(updates)
 
     @classmethod
     def create_studies(cls, studies, server):
         """Create studies"""
 
         pool = Pool()
-        Patient = pool.get('gnuhealth.orthanc.patient')
+        Patient = pool.get("gnuhealth.orthanc.patient")
 
         entries = cls.get_info_from_dicom(studies)
         for entry in entries:
             try:
-                patient = Patient.search([('uuid', '=', entry['parent_patient']),
-                                            ('server', '=', server)], limit=1)[0]
+                patient = Patient.search(
+                    [("uuid", "=", entry["parent_patient"]), ("server", "=", server)],
+                    limit=1,
+                )[0]
             except:
                 patient = None
-                logger.warning('No parent patient found for study {}'.format(entry['ID']))
-            entry.pop('parent_patient')
-            entry['server'] = server
-            entry['patient'] = patient
+                logger.warning(
+                    "No parent patient found for study {}".format(entry["ID"])
+                )
+            entry.pop("parent_patient")  # remove non-model entry
+            entry["server"] = server
+            entry["patient"] = patient
         cls.create(entries)
 
+
 class Orthanc(ModelSQL, ModelView):
-    '''Add Orthanc patient(s) to the main patient data'''
+    """Add Orthanc patient(s) to the main patient data"""
 
-    __name__ = 'gnuhealth.patient'
+    __name__ = "gnuhealth.patient"
 
-    orthanc_patients = fields.One2Many('gnuhealth.orthanc.patient', 'patient', 'Orthanc patients')
+    orthanc_patients = fields.One2Many(
+        "gnuhealth.orthanc.patient", "patient", "Orthanc patients"
+    )
--- a/tryton/health_orthanc/wizard/wizard.py
+++ b/tryton/health_orthanc/wizard/wizard.py
@@ -18,7 +18,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-import logging
 from datetime import datetime
 from trytond.model import ModelView, fields
 from trytond.wizard import Wizard, StateTransition, StateView, Button
@@ -27,55 +26,75 @@
 from requests.auth import HTTPBasicAuth as auth
 from requests.exceptions import HTTPError, ConnectionError
 from orthanc_rest_client import Orthanc as RestClient
+import logging
 
 logger = logging.getLogger(__name__)
 
-__all__ = ['AddOrthancInit', 'FullSyncOrthanc', 'AddOrthancResult']
+__all__ = ["AddOrthancInit", "FullSyncOrthanc", "AddOrthancResult"]
+
 
 class AddOrthancInit(ModelView):
     """Init Full Orthanc Sync"""
-    __name__ = 'gnuhealth.orthanc.add.init'
 
-    label = fields.Char('Label', required=True, help="The label of the Orthanc server. Must be unique")
-    domain = fields.Char('URL', required=True, help="The full URL of the Orthanc server")
-    user = fields.Char('Username', required=True, help="Username for Orthanc REST server")
-    password = fields.Char('Password', required=True, help="Password for Orthanc REST server")
+    __name__ = "gnuhealth.orthanc.add.init"
+
+    label = fields.Char(
+        "Label", required=True, help="The label of the Orthanc server. Must be unique"
+    )
+    domain = fields.Char(
+        "URL", required=True, help="The full URL of the Orthanc server"
+    )
+    user = fields.Char(
+        "Username", required=True, help="Username for Orthanc REST server"
+    )
+    password = fields.Char(
+        "Password", required=True, help="Password for Orthanc REST server"
+    )
+
 
 class AddOrthancResult(ModelView):
     """Display Result"""
-    __name__ = 'gnuhealth.orthanc.add.result'
 
-    result = fields.Text('Result', help="Information")
+    __name__ = "gnuhealth.orthanc.add.result"
+
+    result = fields.Text("Result", help="Information")
+
 
 class FullSyncOrthanc(Wizard):
-    'Full sync new orthanc server'
-    __name__ = 'gnuhealth.orthanc.wizard.full_sync'
+    "Full sync new orthanc server"
+    __name__ = "gnuhealth.orthanc.wizard.full_sync"
 
-    start = StateView('gnuhealth.orthanc.add.init', 'health_orthanc.view_orthanc_add_init',
-            [
-                Button('Cancel', 'end', 'tryton-cancel'),
-                Button('Begin', 'first_sync', 'tryton-ok', default=True),
-            ])
+    start = StateView(
+        "gnuhealth.orthanc.add.init",
+        "health_orthanc.view_orthanc_add_init",
+        [
+            Button("Cancel", "end", "tryton-cancel"),
+            Button("Begin", "first_sync", "tryton-ok", default=True),
+        ],
+    )
 
     first_sync = StateTransition()
-    result = StateView('gnuhealth.orthanc.add.result', 'health_orthanc.view_orthanc_add_result',
-            [
-                Button('Close', 'end', 'tryton-close'),
-            ])
+    result = StateView(
+        "gnuhealth.orthanc.add.result",
+        "health_orthanc.view_orthanc_add_result",
+        [Button("Close", "end", "tryton-close")],
+    )
 
     def transition_first_sync(self):
         """Import and create all current patients and studies on remote DICOM server
         """
 
         pool = Pool()
-        Patient = pool.get('gnuhealth.orthanc.patient')
-        Study = pool.get('gnuhealth.orthanc.study')
-        Config = pool.get('gnuhealth.orthanc.config')
+        Patient = pool.get("gnuhealth.orthanc.patient")
+        Study = pool.get("gnuhealth.orthanc.study")
+        Config = pool.get("gnuhealth.orthanc.config")
 
-        orthanc = RestClient(self.start.domain, auth=auth(self.start.user, self.start.password))
+        orthanc = RestClient(
+            self.start.domain, auth=auth(self.start.user, self.start.password)
+        )
         try:
-            patients = [p for p in orthanc.get_patients(params={'expand': ''})]
-            studies = [s for s in orthanc.get_studies(params={'expand': ''})]
+            patients = [p for p in orthanc.get_patients(params={"expand": ""})]
+            studies = [s for s in orthanc.get_studies(params={"expand": ""})]
         except HTTPError as err:
             if err.response.status_code == 401:
                 self.result.result = "Invalid credentials provided"
@@ -85,24 +104,28 @@
             self.result.result = "Invalid domain provided"
         else:
             new_server = {
-                    'label': self.start.label,
-                    'domain': self.start.domain,
-                    'user': self.start.user,
-                    'password': self.start.password
-                    }
+                "label": self.start.label,
+                "domain": self.start.domain,
+                "user": self.start.user,
+                "password": self.start.password,
+            }
             server, = Config.create([new_server])
             Patient.create_patients(patients, server)
             Study.create_studies(studies, server)
-            server.last = orthanc.get_changes(last=True).get('Last')
+            server.last = orthanc.get_changes(last=True).get("Last")
             server.sync_time = datetime.now()
             server.validated = True
-            logger.info('<{}> sync complete: {} new patients, {} new studies'.format(server.label, len(patients), len(studies)))
+            logger.info(
+                "<{}> sync complete: {} new patients, {} new studies".format(
+                    server.label, len(patients), len(studies)
+                )
+            )
             Config.save([server])
-            self.result.result = "Successfully added and synced <{}>".format(server.label)
+            self.result.result = "Successfully added and synced <{}>".format(
+                server.label
+            )
         finally:
-            return 'result'
+            return "result"
 
     def default_result(self, fields):
-        return {
-                'result': self.result.result
-                }
+        return {"result": self.result.result}