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.16.0</version> 
 36          <!-- ======================================== Eras ======================================== --> 
 37          <eras> 
 38                  <era> 
 39                          <name>%s</name> 
 40                          <start>%s</start> 
 41                          <end>%s</end> 
 42                          <color>205,238,241</color> 
 43                          <ends_today>%s</ends_today> 
 44                  </era> 
 45                  <era> 
 46                          <name>%s</name> 
 47                          <start>%s</start> 
 48                          <end>%s</end> 
 49                          <color>161,210,226</color> 
 50                          <ends_today>%s</ends_today> 
 51                  </era> 
 52          </eras> 
 53          <!-- ======================================== Categories ======================================== --> 
 54          <categories> 
 55                  <!-- health issues --> 
 56                  <category> 
 57                          <name>%s</name> 
 58                          <color>255,0,0</color> 
 59                          <font_color>0,0,0</font_color> 
 60                  </category> 
 61                  <!-- episodes --> 
 62                  <category> 
 63                          <name>%s</name> 
 64                          <color>0,255,0</color> 
 65                          <font_color>0,0,0</font_color> 
 66                  </category> 
 67                  <!-- encounters --> 
 68                  <category> 
 69                          <name>%s</name> 
 70                          <color>30,144,255</color> 
 71                          <font_color>0,0,0</font_color> 
 72                  </category> 
 73                  <!-- hospital stays --> 
 74                  <category> 
 75                          <name>%s</name> 
 76                          <color>255,255,0</color> 
 77                          <font_color>0,0,0</font_color> 
 78                  </category> 
 79                  <!-- procedures --> 
 80                  <category> 
 81                          <name>%s</name> 
 82                          <color>160,32,140</color> 
 83                          <font_color>0,0,0</font_color> 
 84                  </category> 
 85                  <!-- documents --> 
 86                  <category> 
 87                          <name>%s</name> 
 88                          <color>255,165,0</color> 
 89                          <font_color>0,0,0</font_color> 
 90                  </category> 
 91                  <!-- vaccinations --> 
 92                  <category> 
 93                          <name>%s</name> 
 94                          <color>144,238,144</color> 
 95                          <font_color>0,0,0</font_color> 
 96                  </category> 
 97                  <!-- substance intake --> 
 98                  <category> 
 99                          <name>%s</name> 
100                          <color>165,42,42</color> 
101                          <font_color>0,0,0</font_color> 
102                  </category> 
103                  <!-- life events --> 
104                  <category> 
105                          <name>%s</name> 
106                          <color>30,144,255</color> 
107                          <font_color>0,0,0</font_color> 
108                  </category> 
109          </categories> 
110          <!-- ======================================== Events ======================================== --> 
111          <events>""" 
112   
113  xml_end = """ 
114          </events> 
115          <view> 
116                  <displayed_period> 
117                          <start>%s</start> 
118                          <end>%s</end> 
119                  </displayed_period> 
120          <hidden_categories> 
121          </hidden_categories> 
122          </view> 
123  </timeline>""" 
124   
125  #============================================================ 
126 -def format_pydt(pydt, format = '%Y-%m-%d %H:%M:%S'):
127 return gmDateTime.pydt_strftime(pydt, format = format, accuracy = gmDateTime.acc_seconds)
128 129 #------------------------------------------------------------ 130 # health issues 131 #------------------------------------------------------------ 132 __xml_issue_template = """ 133 <event> 134 <start>%(start)s</start> 135 <end>%(end)s</end> 136 <text>%(container_id)s%(label)s</text> 137 <fuzzy>%(fuzzy)s</fuzzy> 138 <locked>True</locked> 139 <ends_today>%(ends2day)s</ends_today> 140 <category>%(category)s</category> 141 <description>%(desc)s</description> 142 </event>""" 143
144 -def __format_health_issue_as_timeline_xml(issue, patient, emr):
145 # container IDs are supposed to be numeric 146 # 85bd7a14a1e74aab8db072ff8f417afb@H30.rldata.local 147 data = {'category': _('Health issues')} 148 possible_start = issue.possible_start_date 149 safe_start = issue.safe_start_date 150 end = issue.clinical_end_date 151 ends_today = 'False' 152 if end is None: 153 # open episode or active 154 ends_today = 'True' 155 end = now 156 data['desc'] = gmTools.xml_escape_string(issue.format ( 157 patient = patient, 158 with_summary = True, 159 with_codes = True, 160 with_episodes = True, 161 with_encounters = False, 162 with_medications = False, 163 with_hospital_stays = False, 164 with_procedures = False, 165 with_family_history = False, 166 with_documents = False, 167 with_tests = False, 168 with_vaccinations = False 169 ).strip().strip('\n').strip()) 170 label = gmTools.shorten_words_in_line(text = issue['description'], max_length = 25, min_word_length = 5) 171 xml = '' 172 # if possible_start < safe_start: 173 # data['start'] = format_pydt(possible_start) 174 # data['end'] = format_pydt(safe_start) 175 # data['ends2day'] = 'False' 176 # data['fuzzy'] = 'True' 177 # data['container_id'] = '' 178 # data['label'] = '?%s?' % gmTools.xml_escape_string(label) 179 # xml += __xml_issue_template % data 180 data['start'] = format_pydt(safe_start) 181 data['end'] = format_pydt(end) 182 data['ends2day'] = ends_today 183 data['fuzzy'] = 'False' 184 data['container_id'] = '[%s]' % issue['pk_health_issue'] 185 data['label'] = gmTools.xml_escape_string(label) 186 xml += __xml_issue_template % data 187 return xml
188 189 #------------------------------------------------------------ 190 # episodes 191 #------------------------------------------------------------ 192 __xml_episode_template = """ 193 <event> 194 <start>%(start)s</start> 195 <end>%(end)s</end> 196 <text>%(container_id)s%(label)s</text> 197 <progress>%(progress)s</progress> 198 <fuzzy>False</fuzzy> 199 <locked>True</locked> 200 <ends_today>%(ends2day)s</ends_today> 201 <category>%(category)s</category> 202 <description>%(desc)s</description> 203 </event>""" 204
205 -def __format_episode_as_timeline_xml(episode, patient):
206 data = { 207 'category': _('Episodes'), 208 'start': format_pydt(episode.best_guess_clinical_start_date), 209 'container_id': gmTools.coalesce(episode['pk_health_issue'], '', '(%s)'), 210 'label': gmTools.xml_escape_string ( 211 gmTools.shorten_words_in_line(text = episode['description'], max_length = 20, min_word_length = 5) 212 ), 213 'ends2day': gmTools.bool2subst(episode['episode_open'], 'True', 'False'), 214 'progress': gmTools.bool2subst(episode['episode_open'], '0', '100'), 215 'desc': gmTools.xml_escape_string(episode.format ( 216 patient = patient, 217 with_summary = True, 218 with_codes = True, 219 with_encounters = True, 220 with_documents = False, 221 with_hospital_stays = False, 222 with_procedures = False, 223 with_family_history = False, 224 with_tests = False, 225 with_vaccinations = False, 226 with_health_issue = True 227 ).strip().strip('\n').strip()) 228 } 229 end = episode.best_guess_clinical_end_date 230 if end is None: 231 data['end'] = format_pydt(now) 232 else: 233 data['end'] = format_pydt(end) 234 return __xml_episode_template % data
235 236 #------------------------------------------------------------ 237 # encounters 238 #------------------------------------------------------------ 239 __xml_encounter_template = """ 240 <event> 241 <start>%s</start> 242 <end>%s</end> 243 <text>%s</text> 244 <progress>0</progress> 245 <fuzzy>False</fuzzy> 246 <locked>True</locked> 247 <ends_today>False</ends_today> 248 <category>%s</category> 249 <description>%s</description> 250 <milestone>%s</milestone> 251 </event>""" 252
253 -def __format_encounter_as_timeline_xml(encounter, patient):
254 return __xml_encounter_template % ( 255 format_pydt(encounter['started']), 256 format_pydt(encounter['last_affirmed']), 257 #u'(%s)' % encounter['pk_episode'], 258 gmTools.xml_escape_string(format_pydt(encounter['started'], format = '%b %d')), 259 _('Encounters'), # category 260 gmTools.xml_escape_string(encounter.format ( 261 patient = patient, 262 with_soap = True, 263 with_docs = False, 264 with_tests = False, 265 fancy_header = False, 266 with_vaccinations = False, 267 with_co_encountlet_hints = False, 268 with_rfe_aoe = True, 269 with_family_history = False 270 ).strip().strip('\n').strip()), 271 'False' 272 )
273 274 #------------------------------------------------------------ 275 # hospital stays 276 #------------------------------------------------------------ 277 __xml_hospital_stay_template = """ 278 <event> 279 <start>%s</start> 280 <end>%s</end> 281 <text>%s</text> 282 <fuzzy>False</fuzzy> 283 <locked>True</locked> 284 <ends_today>False</ends_today> 285 <category>%s</category> 286 <description>%s</description> 287 </event>""" 288
289 -def __format_hospital_stay_as_timeline_xml(stay):
290 end = stay['discharge'] 291 if end is None: 292 end = now 293 return __xml_hospital_stay_template % ( 294 format_pydt(stay['admission']), 295 format_pydt(end), 296 gmTools.xml_escape_string(stay['hospital']), 297 _('Hospital stays'), # category 298 gmTools.xml_escape_string(stay.format().strip().strip('\n').strip()) 299 )
300 301 #------------------------------------------------------------ 302 # procedures 303 #------------------------------------------------------------ 304 __xml_procedure_template = """ 305 <event> 306 <start>%s</start> 307 <end>%s</end> 308 <text>%s</text> 309 <fuzzy>False</fuzzy> 310 <locked>True</locked> 311 <ends_today>False</ends_today> 312 <category>%s</category> 313 <description>%s</description> 314 </event>""" 315
316 -def __format_procedure_as_timeline_xml(proc):
317 if proc['is_ongoing']: 318 end = now 319 else: 320 if proc['clin_end'] is None: 321 end = proc['clin_when'] 322 else: 323 end = proc['clin_end'] 324 desc = gmTools.shorten_words_in_line(text = proc['performed_procedure'], max_length = 20, min_word_length = 5) 325 return __xml_procedure_template % ( 326 format_pydt(proc['clin_when']), 327 format_pydt(end), 328 gmTools.xml_escape_string(desc), 329 _('Procedures'), 330 gmTools.xml_escape_string(proc.format ( 331 include_episode = True, 332 include_codes = True 333 ).strip().strip('\n').strip()) 334 )
335 336 #------------------------------------------------------------ 337 # documents 338 #------------------------------------------------------------ 339 __xml_document_template = """ 340 <event> 341 <start>%s</start> 342 <end>%s</end> 343 <text>%s</text> 344 <fuzzy>False</fuzzy> 345 <locked>True</locked> 346 <ends_today>False</ends_today> 347 <category>%s</category> 348 <description>%s</description> 349 </event>""" 350
351 -def __format_document_as_timeline_xml(doc):
352 desc = gmTools.shorten_words_in_line(text = doc['l10n_type'], max_length = 20, min_word_length = 5) 353 return __xml_document_template % ( 354 format_pydt(doc['clin_when']), 355 format_pydt(doc['clin_when']), 356 gmTools.xml_escape_string(desc), 357 _('Documents'), 358 gmTools.xml_escape_string(doc.format().strip().strip('\n').strip()) 359 )
360 361 #------------------------------------------------------------ 362 # vaccinations 363 #------------------------------------------------------------ 364 __xml_vaccination_template = """ 365 <event> 366 <start>%s</start> 367 <end>%s</end> 368 <text>%s</text> 369 <fuzzy>False</fuzzy> 370 <locked>True</locked> 371 <ends_today>False</ends_today> 372 <category>%s</category> 373 <description>%s</description> 374 </event>""" 375
376 -def __format_vaccination_as_timeline_xml(vacc):
377 return __xml_vaccination_template % ( 378 format_pydt(vacc['date_given']), 379 format_pydt(vacc['date_given']), 380 gmTools.xml_escape_string(vacc['vaccine']), 381 _('Vaccinations'), 382 gmTools.xml_escape_string('\n'.join(vacc.format ( 383 with_indications = True, 384 with_comment = True, 385 with_reaction = True, 386 date_format = '%Y %b %d' 387 )).strip().strip('\n').strip()) 388 )
389 390 #------------------------------------------------------------ 391 # substance intake 392 #------------------------------------------------------------ 393 __xml_intake_template = """ 394 <event> 395 <start>%s</start> 396 <end>%s</end> 397 <text>%s</text> 398 <fuzzy>False</fuzzy> 399 <locked>True</locked> 400 <ends_today>False</ends_today> 401 <category>%s</category> 402 <description>%s</description> 403 </event>""" 404
405 -def __format_intake_as_timeline_xml(intake):
406 if intake['discontinued'] is None: 407 if intake['duration'] is None: 408 if intake['seems_inactive']: 409 end = intake['started'] 410 else: 411 end = now 412 else: 413 end = intake['started'] + intake['duration'] 414 else: 415 end = intake['discontinued'] 416 417 return __xml_intake_template % ( 418 format_pydt(intake['started']), 419 format_pydt(end), 420 gmTools.xml_escape_string(intake['substance']), 421 _('Substances'), 422 gmTools.xml_escape_string(intake.format ( 423 single_line = False, 424 show_all_product_components = False 425 ).strip().strip('\n').strip()) 426 )
427 428 #------------------------------------------------------------ 429 # main library entry point 430 #------------------------------------------------------------
431 -def create_timeline_file(patient=None, filename=None, include_documents=False, include_vaccinations=False, include_encounters=False):
432 433 emr = patient.emr 434 global now 435 now = gmDateTime.pydt_now_here() 436 437 if filename is None: 438 timeline_fname = gmTools.get_unique_filename(prefix = 'gm-', suffix = '.timeline') # .timeline required ... 439 else: 440 timeline_fname = filename 441 _log.debug('exporting EMR as timeline into [%s]', timeline_fname) 442 timeline = io.open(timeline_fname, mode = 'wt', encoding = 'utf8', errors = 'xmlcharrefreplace') 443 444 if patient['dob'] is None: 445 lifespan_start = format_pydt(now.replace(year = now.year - 100)) 446 else: 447 lifespan_start = format_pydt(patient['dob']) 448 449 if patient['deceased'] is None: 450 life_ends2day = 'True' 451 lifespan_end = format_pydt(now) 452 else: 453 life_ends2day = 'False' 454 lifespan_end = format_pydt(patient['deceased']) 455 456 earliest_care_date = emr.earliest_care_date 457 most_recent_care_date = emr.most_recent_care_date 458 if most_recent_care_date is None: 459 most_recent_care_date = lifespan_end 460 care_ends2day = life_ends2day 461 else: 462 most_recent_care_date = format_pydt(most_recent_care_date) 463 care_ends2day = 'False' 464 465 timeline.write(xml_start % ( 466 # era: life span of patient 467 _('Lifespan'), 468 lifespan_start, 469 lifespan_end, 470 life_ends2day, 471 ERA_NAME_CARE_PERIOD, 472 format_pydt(earliest_care_date), 473 most_recent_care_date, 474 care_ends2day, 475 # categories 476 _('Health issues'), 477 _('Episodes'), 478 _('Encounters'), 479 _('Hospital stays'), 480 _('Procedures'), 481 _('Documents'), 482 _('Vaccinations'), 483 _('Substances'), 484 _('Life events') 485 )) 486 # birth 487 if patient['dob'] is None: 488 start = now.replace(year = now.year - 100) 489 timeline.write(__xml_encounter_template % ( 490 format_pydt(start), 491 format_pydt(start), 492 '?', 493 _('Life events'), 494 _('Date of birth unknown'), 495 'True' 496 )) 497 else: 498 start = patient['dob'] 499 timeline.write(__xml_encounter_template % ( 500 format_pydt(patient['dob']), 501 format_pydt(patient['dob']), 502 '*', 503 _('Life events'), 504 '%s: %s (%s)' % ( 505 _('Birth'), 506 patient.get_formatted_dob(format = '%Y %b %d %H:%M', honor_estimation = True), 507 patient.get_medical_age() 508 ), 509 'True' 510 )) 511 512 # start of care 513 timeline.write(__xml_encounter_template % ( 514 format_pydt(earliest_care_date), 515 format_pydt(earliest_care_date), 516 gmTools.u_heavy_greek_cross, 517 _('Life events'), 518 _('Start of Care: %s\n(the earliest recorded event of care in this praxis)') % format_pydt(earliest_care_date, format = '%Y %b %d'), 519 'True' 520 )) 521 522 # containers must be defined before their 523 # subevents, so put health issues first 524 timeline.write('\n <!-- ========================================\n Health issues\n======================================== -->') 525 for issue in emr.health_issues: 526 timeline.write(__format_health_issue_as_timeline_xml(issue, patient, emr)) 527 528 timeline.write('\n <!-- ========================================\n Episodes\n======================================== -->') 529 for epi in emr.get_episodes(order_by = 'pk_health_issue'): 530 timeline.write(__format_episode_as_timeline_xml(epi, patient)) 531 532 if include_encounters: 533 timeline.write(u'\n<!--\n========================================\n Encounters\n======================================== -->') 534 for enc in emr.get_encounters(skip_empty = True): 535 timeline.write(__format_encounter_as_timeline_xml(enc, patient)) 536 537 timeline.write('\n<!--\n========================================\n Hospital stays\n======================================== -->') 538 for stay in emr.hospital_stays: 539 timeline.write(__format_hospital_stay_as_timeline_xml(stay)) 540 541 timeline.write('\n<!--\n========================================\n Procedures\n======================================== -->') 542 for proc in emr.performed_procedures: 543 timeline.write(__format_procedure_as_timeline_xml(proc)) 544 545 if include_vaccinations: 546 timeline.write(u'\n<!--\n========================================\n Vaccinations\n======================================== -->') 547 for vacc in emr.vaccinations: 548 timeline.write(__format_vaccination_as_timeline_xml(vacc)) 549 550 timeline.write('\n<!--\n========================================\n Substance intakes\n======================================== -->') 551 for intake in emr.get_current_medications(include_inactive = True, include_unapproved = False): 552 timeline.write(__format_intake_as_timeline_xml(intake)) 553 554 if include_documents: 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 gmI18N.activate_locale() 710 gmI18N.install_domain('gnumed') 711 712 from Gnumed.business import gmPraxis 713 praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0]) 714 715 from Gnumed.business import gmPerson 716 # 14 / 20 / 138 / 58 / 20 / 5 717 pat = gmPerson.gmCurrentPatient(gmPerson.cPatient(aPK_obj = 14)) 718 fname = '~/gnumed/gm2tl-%s.timeline' % pat.subdir_name 719 720 print (create_timeline_file ( 721 patient = pat, 722 filename = os.path.expanduser(fname), 723 include_documents = True, 724 include_vaccinations = True, 725 include_encounters = True 726 )) 727