Package Gnumed :: Package exporters :: Module gmTimelineExporter
[frames] | no frames]

Source Code for Module Gnumed.exporters.gmTimelineExporter

  1  # -*- coding: utf8 -*- 
  2  """Timeline exporter. 
  3   
  4  Copyright: authors 
  5  """ 
  6  #============================================================ 
  7  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
  8  __license__ = 'GPL v2 or later (details at http://www.gnu.org)' 
  9   
 10  import sys 
 11  import logging 
 12  import io 
 13  import os 
 14   
 15   
 16  if __name__ == '__main__': 
 17          sys.path.insert(0, '../../') 
 18  from Gnumed.pycommon import gmTools 
 19  from Gnumed.pycommon import gmDateTime 
 20   
 21   
 22  _log = logging.getLogger('gm.tl') 
 23   
 24  #============================================================ 
 25  if __name__ == '__main__': 
 26          _ = lambda x:x 
 27   
 28  ERA_NAME_CARE_PERIOD = _('Care Period') 
 29   
 30  #============================================================ 
 31   
 32  # <icon>base-64 encoded PNG image data</icon> 
 33   
 34  #============================================================ 
 35  xml_start = """<?xml version="1.0" encoding="utf-8"?> 
 36  <timeline> 
 37          <version>1.16.0</version> 
 38          <!-- ======================================== Eras ======================================== --> 
 39          <eras> 
 40                  <era> 
 41                          <name>%s</name> 
 42                          <start>%s</start> 
 43                          <end>%s</end> 
 44                          <color>205,238,241</color> 
 45                          <ends_today>%s</ends_today> 
 46                  </era> 
 47                  <era> 
 48                          <name>%s</name> 
 49                          <start>%s</start> 
 50                          <end>%s</end> 
 51                          <color>161,210,226</color> 
 52                          <ends_today>%s</ends_today> 
 53                  </era> 
 54          </eras> 
 55          <!-- ======================================== Categories ======================================== --> 
 56          <categories> 
 57                  <!-- health issues --> 
 58                  <category> 
 59                          <name>%s</name> 
 60                          <color>255,0,0</color> 
 61                          <font_color>0,0,0</font_color> 
 62                  </category> 
 63                  <!-- episodes --> 
 64                  <category> 
 65                          <name>%s</name> 
 66                          <color>0,255,0</color> 
 67                          <font_color>0,0,0</font_color> 
 68                  </category> 
 69                  <!-- encounters --> 
 70                  <category> 
 71                          <name>%s</name> 
 72                          <color>30,144,255</color> 
 73                          <font_color>0,0,0</font_color> 
 74                  </category> 
 75                  <!-- hospital stays --> 
 76                  <category> 
 77                          <name>%s</name> 
 78                          <color>255,255,0</color> 
 79                          <font_color>0,0,0</font_color> 
 80                  </category> 
 81                  <!-- procedures --> 
 82                  <category> 
 83                          <name>%s</name> 
 84                          <color>160,32,140</color> 
 85                          <font_color>0,0,0</font_color> 
 86                  </category> 
 87                  <!-- documents --> 
 88                  <category> 
 89                          <name>%s</name> 
 90                          <color>255,165,0</color> 
 91                          <font_color>0,0,0</font_color> 
 92                  </category> 
 93                  <!-- vaccinations --> 
 94                  <category> 
 95                          <name>%s</name> 
 96                          <color>144,238,144</color> 
 97                          <font_color>0,0,0</font_color> 
 98                  </category> 
 99                  <!-- substance intake --> 
100                  <category> 
101                          <name>%s</name> 
102                          <color>165,42,42</color> 
103                          <font_color>0,0,0</font_color> 
104                  </category> 
105                  <!-- life events --> 
106                  <category> 
107                          <name>%s</name> 
108                          <color>30,144,255</color> 
109                          <font_color>0,0,0</font_color> 
110                  </category> 
111          </categories> 
112          <!-- ======================================== Events ======================================== --> 
113          <events>""" 
114   
115  xml_end = """ 
116          </events> 
117          <view> 
118                  <displayed_period> 
119                          <start>%s</start> 
120                          <end>%s</end> 
121                  </displayed_period> 
122          <hidden_categories> 
123          </hidden_categories> 
124          </view> 
125  </timeline>""" 
126   
127  #============================================================ 
128 -def format_pydt(pydt, format = '%Y-%m-%d %H:%M:%S'):
129 return gmDateTime.pydt_strftime(pydt, format = format, accuracy = gmDateTime.acc_seconds)
130 131 #------------------------------------------------------------ 132 # health issues 133 #------------------------------------------------------------ 134 __xml_issue_template = """ 135 <event> 136 <start>%(start)s</start> 137 <end>%(end)s</end> 138 <text>%(container_id)s%(label)s</text> 139 <fuzzy>%(fuzzy)s</fuzzy> 140 <locked>True</locked> 141 <ends_today>%(ends2day)s</ends_today> 142 <category>%(category)s</category> 143 <description>%(desc)s</description> 144 </event>""" 145
146 -def __format_health_issue_as_timeline_xml(issue, patient, emr):
147 # container IDs are supposed to be numeric 148 # 85bd7a14a1e74aab8db072ff8f417afb@H30.rldata.local 149 data = {'category': _('Health issues')} 150 possible_start = issue.possible_start_date 151 safe_start = issue.safe_start_date 152 end = issue.clinical_end_date 153 ends_today = 'False' 154 if end is None: 155 # open episode or active 156 ends_today = 'True' 157 end = now 158 data['desc'] = gmTools.xml_escape_string(issue.format ( 159 patient = patient, 160 with_summary = True, 161 with_codes = True, 162 with_episodes = True, 163 with_encounters = False, 164 with_medications = False, 165 with_hospital_stays = False, 166 with_procedures = False, 167 with_family_history = False, 168 with_documents = False, 169 with_tests = False, 170 with_vaccinations = False 171 ).strip().strip('\n').strip()) 172 txt = gmTools.shorten_words_in_line(text = issue['description'], max_length = 25, min_word_length = 5) 173 xml = '' 174 if possible_start < safe_start: 175 data['start'] = format_pydt(possible_start) 176 data['end'] = format_pydt(safe_start) 177 data['ends2day'] = 'False' 178 data['fuzzy'] = 'True' 179 data['container_id'] = '' 180 data['label'] = '?%s?' % gmTools.xml_escape_string(txt) 181 xml += __xml_issue_template % data 182 data['start'] = format_pydt(safe_start) 183 data['end'] = format_pydt(end) 184 data['ends2day'] = ends_today 185 data['fuzzy'] = 'False' 186 data['container_id'] = '[%s]' % issue['pk_health_issue'] 187 data['label'] = gmTools.xml_escape_string(txt) 188 xml += __xml_issue_template % data 189 return xml
190 191 #------------------------------------------------------------ 192 # episodes 193 #------------------------------------------------------------ 194 __xml_episode_template = """ 195 <event> 196 <start>%(start)s</start> 197 <end>%(end)s</end> 198 <text>%(container_id)s%(label)s</text> 199 <progress>%(progress)s</progress> 200 <fuzzy>False</fuzzy> 201 <locked>True</locked> 202 <ends_today>%(ends2day)s</ends_today> 203 <category>%(category)s</category> 204 <description>%(desc)s</description> 205 </event>""" 206
207 -def __format_episode_as_timeline_xml(episode, patient):
208 data = { 209 'category': _('Episodes'), 210 'start': format_pydt(episode.best_guess_clinical_start_date), 211 'container_id': gmTools.coalesce(episode['pk_health_issue'], '', '(%s)'), 212 'label': gmTools.xml_escape_string ( 213 gmTools.shorten_words_in_line(text = episode['description'], max_length = 20, min_word_length = 5) 214 ), 215 'ends2day': gmTools.bool2subst(episode['episode_open'], 'True', 'False'), 216 'progress': gmTools.bool2subst(episode['episode_open'], '0', '100'), 217 'desc': gmTools.xml_escape_string(episode.format ( 218 patient = patient, 219 with_summary = True, 220 with_codes = True, 221 with_encounters = True, 222 with_documents = False, 223 with_hospital_stays = False, 224 with_procedures = False, 225 with_family_history = False, 226 with_tests = False, 227 with_vaccinations = False, 228 with_health_issue = True 229 ).strip().strip('\n').strip()) 230 } 231 end = episode.best_guess_clinical_end_date 232 if end is None: 233 data['end'] = format_pydt(now) 234 else: 235 data['end'] = format_pydt(end) 236 return __xml_episode_template % data
237 238 #------------------------------------------------------------ 239 # encounters 240 #------------------------------------------------------------ 241 __xml_encounter_template = """ 242 <event> 243 <start>%s</start> 244 <end>%s</end> 245 <text>%s</text> 246 <progress>0</progress> 247 <fuzzy>False</fuzzy> 248 <locked>True</locked> 249 <ends_today>False</ends_today> 250 <category>%s</category> 251 <description>%s</description> 252 <milestone>%s</milestone> 253 </event>""" 254
255 -def __format_encounter_as_timeline_xml(encounter, patient):
256 return __xml_encounter_template % ( 257 format_pydt(encounter['started']), 258 format_pydt(encounter['last_affirmed']), 259 #u'(%s)' % encounter['pk_episode'], 260 gmTools.xml_escape_string(format_pydt(encounter['started'], format = '%b %d')), 261 _('Encounters'), # category 262 gmTools.xml_escape_string(encounter.format ( 263 patient = patient, 264 with_soap = True, 265 with_docs = False, 266 with_tests = False, 267 fancy_header = False, 268 with_vaccinations = False, 269 with_co_encountlet_hints = False, 270 with_rfe_aoe = True, 271 with_family_history = False 272 ).strip().strip('\n').strip()), 273 'False' 274 )
275 276 #------------------------------------------------------------ 277 # hospital stays 278 #------------------------------------------------------------ 279 __xml_hospital_stay_template = """ 280 <event> 281 <start>%s</start> 282 <end>%s</end> 283 <text>%s</text> 284 <fuzzy>False</fuzzy> 285 <locked>True</locked> 286 <ends_today>False</ends_today> 287 <category>%s</category> 288 <description>%s</description> 289 </event>""" 290
291 -def __format_hospital_stay_as_timeline_xml(stay):
292 end = stay['discharge'] 293 if end is None: 294 end = now 295 return __xml_hospital_stay_template % ( 296 format_pydt(stay['admission']), 297 format_pydt(end), 298 gmTools.xml_escape_string(stay['hospital']), 299 _('Hospital stays'), # category 300 gmTools.xml_escape_string(stay.format().strip().strip('\n').strip()) 301 )
302 303 #------------------------------------------------------------ 304 # procedures 305 #------------------------------------------------------------ 306 __xml_procedure_template = """ 307 <event> 308 <start>%s</start> 309 <end>%s</end> 310 <text>%s</text> 311 <fuzzy>False</fuzzy> 312 <locked>True</locked> 313 <ends_today>False</ends_today> 314 <category>%s</category> 315 <description>%s</description> 316 </event>""" 317
318 -def __format_procedure_as_timeline_xml(proc):
319 if proc['is_ongoing']: 320 end = now 321 else: 322 if proc['clin_end'] is None: 323 end = proc['clin_when'] 324 else: 325 end = proc['clin_end'] 326 desc = gmTools.shorten_words_in_line(text = proc['performed_procedure'], max_length = 20, min_word_length = 5) 327 return __xml_procedure_template % ( 328 format_pydt(proc['clin_when']), 329 format_pydt(end), 330 gmTools.xml_escape_string(desc), 331 _('Procedures'), 332 gmTools.xml_escape_string(proc.format ( 333 include_episode = True, 334 include_codes = True 335 ).strip().strip('\n').strip()) 336 )
337 338 #------------------------------------------------------------ 339 # documents 340 #------------------------------------------------------------ 341 __xml_document_template = """ 342 <event> 343 <start>%s</start> 344 <end>%s</end> 345 <text>%s</text> 346 <fuzzy>False</fuzzy> 347 <locked>True</locked> 348 <ends_today>False</ends_today> 349 <category>%s</category> 350 <description>%s</description> 351 </event>""" 352
353 -def __format_document_as_timeline_xml(doc):
354 desc = gmTools.shorten_words_in_line(text = doc['l10n_type'], max_length = 20, min_word_length = 5) 355 return __xml_document_template % ( 356 format_pydt(doc['clin_when']), 357 format_pydt(doc['clin_when']), 358 gmTools.xml_escape_string(desc), 359 _('Documents'), 360 gmTools.xml_escape_string(doc.format().strip().strip('\n').strip()) 361 )
362 363 #------------------------------------------------------------ 364 # vaccinations 365 #------------------------------------------------------------ 366 __xml_vaccination_template = """ 367 <event> 368 <start>%s</start> 369 <end>%s</end> 370 <text>%s</text> 371 <fuzzy>False</fuzzy> 372 <locked>True</locked> 373 <ends_today>False</ends_today> 374 <category>%s</category> 375 <description>%s</description> 376 </event>""" 377
378 -def __format_vaccination_as_timeline_xml(vacc):
379 return __xml_vaccination_template % ( 380 format_pydt(vacc['date_given']), 381 format_pydt(vacc['date_given']), 382 gmTools.xml_escape_string(vacc['vaccine']), 383 _('Vaccinations'), 384 gmTools.xml_escape_string('\n'.join(vacc.format ( 385 with_indications = True, 386 with_comment = True, 387 with_reaction = True, 388 date_format = '%Y %b %d' 389 )).strip().strip('\n').strip()) 390 )
391 392 #------------------------------------------------------------ 393 # substance intake 394 #------------------------------------------------------------ 395 __xml_intake_template = """ 396 <event> 397 <start>%s</start> 398 <end>%s</end> 399 <text>%s</text> 400 <fuzzy>False</fuzzy> 401 <locked>True</locked> 402 <ends_today>False</ends_today> 403 <category>%s</category> 404 <description>%s</description> 405 </event>""" 406
407 -def __format_intake_as_timeline_xml(intake):
408 if intake['discontinued'] is None: 409 if intake['duration'] is None: 410 if intake['seems_inactive']: 411 end = intake['started'] 412 else: 413 end = now 414 else: 415 end = intake['started'] + intake['duration'] 416 else: 417 end = intake['discontinued'] 418 419 return __xml_intake_template % ( 420 format_pydt(intake['started']), 421 format_pydt(end), 422 gmTools.xml_escape_string(intake['substance']), 423 _('Substances'), 424 gmTools.xml_escape_string(intake.format ( 425 single_line = False, 426 show_all_product_components = False 427 ).strip().strip('\n').strip()) 428 )
429 430 #------------------------------------------------------------ 431 # main library entry point 432 #------------------------------------------------------------
433 -def create_timeline_file(patient=None, filename=None):
434 435 emr = patient.emr 436 global now 437 now = gmDateTime.pydt_now_here() 438 439 if filename is None: 440 timeline_fname = gmTools.get_unique_filename(prefix = 'gm-', suffix = '.timeline') # .timeline required ... 441 else: 442 timeline_fname = filename 443 _log.debug('exporting EMR as timeline into [%s]', timeline_fname) 444 timeline = io.open(timeline_fname, mode = 'wt', encoding = 'utf8', errors = 'xmlcharrefreplace') 445 446 if patient['dob'] is None: 447 lifespan_start = format_pydt(now.replace(year = now.year - 100)) 448 else: 449 lifespan_start = format_pydt(patient['dob']) 450 451 if patient['deceased'] is None: 452 life_ends2day = 'True' 453 lifespan_end = format_pydt(now) 454 else: 455 life_ends2day = 'False' 456 lifespan_end = format_pydt(patient['deceased']) 457 458 earliest_care_date = emr.earliest_care_date 459 most_recent_care_date = emr.most_recent_care_date 460 if most_recent_care_date is None: 461 most_recent_care_date = lifespan_end 462 care_ends2day = life_ends2day 463 else: 464 most_recent_care_date = format_pydt(most_recent_care_date) 465 care_ends2day = 'False' 466 467 timeline.write(xml_start % ( 468 # era: life span of patient 469 _('Lifespan'), 470 lifespan_start, 471 lifespan_end, 472 life_ends2day, 473 ERA_NAME_CARE_PERIOD, 474 format_pydt(earliest_care_date), 475 most_recent_care_date, 476 care_ends2day, 477 # categories 478 _('Health issues'), 479 _('Episodes'), 480 _('Encounters'), 481 _('Hospital stays'), 482 _('Procedures'), 483 _('Documents'), 484 _('Vaccinations'), 485 _('Substances'), 486 _('Life events') 487 )) 488 # birth 489 if patient['dob'] is None: 490 start = now.replace(year = now.year - 100) 491 timeline.write(__xml_encounter_template % ( 492 format_pydt(start), 493 format_pydt(start), 494 '?', 495 _('Life events'), 496 _('Date of birth unknown'), 497 'True' 498 )) 499 else: 500 start = patient['dob'] 501 timeline.write(__xml_encounter_template % ( 502 format_pydt(patient['dob']), 503 format_pydt(patient['dob']), 504 '*', 505 _('Life events'), 506 '%s: %s (%s)' % ( 507 _('Birth'), 508 patient.get_formatted_dob(format = '%Y %b %d %H:%M', honor_estimation = True), 509 patient.get_medical_age() 510 ), 511 'True' 512 )) 513 514 # start of care 515 timeline.write(__xml_encounter_template % ( 516 format_pydt(earliest_care_date), 517 format_pydt(earliest_care_date), 518 gmTools.u_heavy_greek_cross, 519 _('Life events'), 520 _('Start of Care: %s\n(the earliest recorded event of care in this praxis)') % format_pydt(earliest_care_date, format = '%Y %b %d'), 521 'True' 522 )) 523 524 # containers must be defined before their 525 # subevents, so put health issues first 526 timeline.write('\n <!-- ========================================\n Health issues\n======================================== -->') 527 for issue in emr.health_issues: 528 timeline.write(__format_health_issue_as_timeline_xml(issue, patient, emr)) 529 530 timeline.write('\n <!-- ========================================\n Episodes\n======================================== -->') 531 for epi in emr.get_episodes(order_by = 'pk_health_issue'): 532 timeline.write(__format_episode_as_timeline_xml(epi, patient)) 533 534 # simply too many 535 #timeline.write(u'\n<!--\n========================================\n Encounters\n======================================== -->') 536 #for enc in emr.get_encounters(skip_empty = True): 537 # timeline.write(__format_encounter_as_timeline_xml(enc, patient)) 538 539 timeline.write('\n<!--\n========================================\n Hospital stays\n======================================== -->') 540 for stay in emr.hospital_stays: 541 timeline.write(__format_hospital_stay_as_timeline_xml(stay)) 542 543 timeline.write('\n<!--\n========================================\n Procedures\n======================================== -->') 544 for proc in emr.performed_procedures: 545 timeline.write(__format_procedure_as_timeline_xml(proc)) 546 547 # timeline.write(u'\n<!--\n========================================\n Vaccinations\n======================================== -->') 548 # for vacc in emr.vaccinations: 549 # timeline.write(__format_vaccination_as_timeline_xml(vacc)) 550 551 timeline.write('\n<!--\n========================================\n Substance intakes\n======================================== -->') 552 for intake in emr.get_current_medications(include_inactive = True, include_unapproved = False): 553 timeline.write(__format_intake_as_timeline_xml(intake)) 554 555 # timeline.write(u'\n<!--\n========================================\n Documents\n======================================== -->') 556 # for doc in patient.document_folder.documents: 557 # timeline.write(__format_document_as_timeline_xml(doc)) 558 559 # allergies ? 560 # - unclear where and how to place 561 # test results ? 562 # - too many events, at most "day sample drawn" 563 564 # death 565 if patient['deceased'] is None: 566 end = now 567 else: 568 end = patient['deceased'] 569 timeline.write(__xml_encounter_template % ( 570 format_pydt(end), 571 format_pydt(end), 572 gmTools.u_dagger, 573 _('Life events'), 574 _('Death: %s') % format_pydt(end, format = '%Y %b %d %H:%M') 575 )) 576 577 # display range 578 if end.month == 2: 579 if end.day == 29: 580 # leap years aren't consecutive 581 end = end.replace(day = 28) 582 target_year = end.year + 1 583 end = end.replace(year = target_year) 584 timeline.write(xml_end % ( 585 format_pydt(start), 586 format_pydt(end) 587 )) 588 589 timeline.close() 590 return timeline_fname
591 592 #------------------------------------------------------------ 593 __fake_timeline_start = """<?xml version="1.0" encoding="utf-8"?> 594 <timeline> 595 <version>0.20.0</version> 596 <categories> 597 <!-- life events --> 598 <category> 599 <name>%s</name> 600 <color>30,144,255</color> 601 <font_color>0,0,0</font_color> 602 </category> 603 </categories> 604 <events>""" % _('Life events') 605 606 __fake_timeline_body_template = """ 607 <event> 608 <start>%s</start> 609 <end>%s</end> 610 <text>%s</text> 611 <fuzzy>False</fuzzy> 612 <locked>True</locked> 613 <ends_today>False</ends_today> 614 <!-- category></category --> 615 <description>%s 616 </description> 617 </event>""" 618
619 -def create_fake_timeline_file(patient=None, filename=None):
620 """Used to create an 'empty' timeline file for display. 621 622 - needed because .clear_timeline() doesn't really work 623 """ 624 emr = patient.emr 625 global now 626 now = gmDateTime.pydt_now_here() 627 628 if filename is None: 629 timeline_fname = gmTools.get_unique_filename(prefix = 'gm-', suffix = '.timeline') 630 else: 631 timeline_fname = filename 632 633 _log.debug('creating dummy timeline in [%s]', timeline_fname) 634 timeline = io.open(timeline_fname, mode = 'wt', encoding = 'utf8', errors = 'xmlcharrefreplace') 635 636 timeline.write(__fake_timeline_start) 637 638 # birth 639 if patient['dob'] is None: 640 start = now.replace(year = now.year - 100) 641 timeline.write(__xml_encounter_template % ( 642 format_pydt(start), 643 format_pydt(start), 644 _('Birth') + ': ?', 645 _('Life events'), 646 _('Date of birth unknown'), 647 'False' 648 )) 649 else: 650 start = patient['dob'] 651 timeline.write(__xml_encounter_template % ( 652 format_pydt(patient['dob']), 653 format_pydt(patient['dob']), 654 _('Birth') + gmTools.bool2subst(patient['dob_is_estimated'], ' (%s)' % gmTools.u_almost_equal_to, ''), 655 _('Life events'), 656 '', 657 'False' 658 )) 659 660 # death 661 if patient['deceased'] is None: 662 end = now 663 else: 664 end = patient['deceased'] 665 timeline.write(__xml_encounter_template % ( 666 format_pydt(end), 667 format_pydt(end), 668 #u'', 669 _('Death'), 670 _('Life events'), 671 '', 672 'False' 673 )) 674 675 # fake issue 676 timeline.write(__fake_timeline_body_template % ( 677 format_pydt(start), 678 format_pydt(end), 679 _('Cannot display timeline.'), 680 _('Cannot display timeline.') 681 )) 682 683 # display range 684 if end.month == 2: 685 if end.day == 29: 686 # leap years aren't consecutive 687 end = end.replace(day = 28) 688 target_year = end.year + 1 689 end = end.replace(year = target_year) 690 timeline.write(xml_end % ( 691 format_pydt(start), 692 format_pydt(end) 693 )) 694 695 timeline.close() 696 return timeline_fname
697 698 #============================================================ 699 # main 700 #------------------------------------------------------------ 701 if __name__ == '__main__': 702 703 if len(sys.argv) < 2: 704 sys.exit() 705 706 if sys.argv[1] != "test": 707 sys.exit() 708 709 from Gnumed.pycommon import gmI18N 710 gmI18N.activate_locale() 711 gmI18N.install_domain('gnumed') 712 713 from Gnumed.business import gmPraxis 714 praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0]) 715 716 from Gnumed.business import gmPerson 717 # 14 / 20 / 138 / 58 / 20 / 5 718 pat = gmPerson.gmCurrentPatient(gmPerson.cPatient(aPK_obj = 14)) 719 fname = '~/gnumed/gm2tl-%s.timeline' % pat.subdir_name 720 721 print(create_timeline_file(patient = pat, filename = os.path.expanduser(fname))) 722