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 label = 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(label) 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(label) 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, include_documents=False, include_vaccinations=False, include_encounters=False):
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 if include_encounters: 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 if include_vaccinations: 548 timeline.write(u'\n<!--\n========================================\n Vaccinations\n======================================== -->') 549 for vacc in emr.vaccinations: 550 timeline.write(__format_vaccination_as_timeline_xml(vacc)) 551 552 timeline.write('\n<!--\n========================================\n Substance intakes\n======================================== -->') 553 for intake in emr.get_current_medications(include_inactive = True, include_unapproved = False): 554 timeline.write(__format_intake_as_timeline_xml(intake)) 555 556 if include_documents: 557 timeline.write(u'\n<!--\n========================================\n Documents\n======================================== -->') 558 for doc in patient.document_folder.documents: 559 timeline.write(__format_document_as_timeline_xml(doc)) 560 561 # allergies ? 562 # - unclear where and how to place 563 # test results ? 564 # - too many events, at most "day sample drawn" 565 566 # death 567 if patient['deceased'] is None: 568 end = now 569 else: 570 end = patient['deceased'] 571 timeline.write(__xml_encounter_template % ( 572 format_pydt(end), 573 format_pydt(end), 574 gmTools.u_dagger, 575 _('Life events'), 576 _('Death: %s') % format_pydt(end, format = '%Y %b %d %H:%M') 577 )) 578 579 # display range 580 if end.month == 2: 581 if end.day == 29: 582 # leap years aren't consecutive 583 end = end.replace(day = 28) 584 target_year = end.year + 1 585 end = end.replace(year = target_year) 586 timeline.write(xml_end % ( 587 format_pydt(start), 588 format_pydt(end) 589 )) 590 591 timeline.close() 592 return timeline_fname
593 594 #------------------------------------------------------------ 595 __fake_timeline_start = """<?xml version="1.0" encoding="utf-8"?> 596 <timeline> 597 <version>0.20.0</version> 598 <categories> 599 <!-- life events --> 600 <category> 601 <name>%s</name> 602 <color>30,144,255</color> 603 <font_color>0,0,0</font_color> 604 </category> 605 </categories> 606 <events>""" % _('Life events') 607 608 __fake_timeline_body_template = """ 609 <event> 610 <start>%s</start> 611 <end>%s</end> 612 <text>%s</text> 613 <fuzzy>False</fuzzy> 614 <locked>True</locked> 615 <ends_today>False</ends_today> 616 <!-- category></category --> 617 <description>%s 618 </description> 619 </event>""" 620
621 -def create_fake_timeline_file(patient=None, filename=None):
622 """Used to create an 'empty' timeline file for display. 623 624 - needed because .clear_timeline() doesn't really work 625 """ 626 emr = patient.emr 627 global now 628 now = gmDateTime.pydt_now_here() 629 630 if filename is None: 631 timeline_fname = gmTools.get_unique_filename(prefix = 'gm-', suffix = '.timeline') 632 else: 633 timeline_fname = filename 634 635 _log.debug('creating dummy timeline in [%s]', timeline_fname) 636 timeline = io.open(timeline_fname, mode = 'wt', encoding = 'utf8', errors = 'xmlcharrefreplace') 637 638 timeline.write(__fake_timeline_start) 639 640 # birth 641 if patient['dob'] is None: 642 start = now.replace(year = now.year - 100) 643 timeline.write(__xml_encounter_template % ( 644 format_pydt(start), 645 format_pydt(start), 646 _('Birth') + ': ?', 647 _('Life events'), 648 _('Date of birth unknown'), 649 'False' 650 )) 651 else: 652 start = patient['dob'] 653 timeline.write(__xml_encounter_template % ( 654 format_pydt(patient['dob']), 655 format_pydt(patient['dob']), 656 _('Birth') + gmTools.bool2subst(patient['dob_is_estimated'], ' (%s)' % gmTools.u_almost_equal_to, ''), 657 _('Life events'), 658 '', 659 'False' 660 )) 661 662 # death 663 if patient['deceased'] is None: 664 end = now 665 else: 666 end = patient['deceased'] 667 timeline.write(__xml_encounter_template % ( 668 format_pydt(end), 669 format_pydt(end), 670 #u'', 671 _('Death'), 672 _('Life events'), 673 '', 674 'False' 675 )) 676 677 # fake issue 678 timeline.write(__fake_timeline_body_template % ( 679 format_pydt(start), 680 format_pydt(end), 681 _('Cannot display timeline.'), 682 _('Cannot display timeline.') 683 )) 684 685 # display range 686 if end.month == 2: 687 if end.day == 29: 688 # leap years aren't consecutive 689 end = end.replace(day = 28) 690 target_year = end.year + 1 691 end = end.replace(year = target_year) 692 timeline.write(xml_end % ( 693 format_pydt(start), 694 format_pydt(end) 695 )) 696 697 timeline.close() 698 return timeline_fname
699 700 #============================================================ 701 # main 702 #------------------------------------------------------------ 703 if __name__ == '__main__': 704 705 if len(sys.argv) < 2: 706 sys.exit() 707 708 if sys.argv[1] != "test": 709 sys.exit() 710 711 from Gnumed.pycommon import gmI18N 712 gmI18N.activate_locale() 713 gmI18N.install_domain('gnumed') 714 715 from Gnumed.business import gmPraxis 716 praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0]) 717 718 from Gnumed.business import gmPerson 719 # 14 / 20 / 138 / 58 / 20 / 5 720 pat = gmPerson.gmCurrentPatient(gmPerson.cPatient(aPK_obj = 14)) 721 fname = '~/gnumed/gm2tl-%s.timeline' % pat.subdir_name 722 723 print (create_timeline_file ( 724 patient = pat, 725 filename = os.path.expanduser(fname), 726 include_documents = True, 727 include_vaccinations = True, 728 include_encounters = True 729 )) 730