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