Gnumed.exporters.gmTimelineExporter

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