| Home | Trees | Indices | Help |
|
|---|
|
|
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 #============================================================
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
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
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
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
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
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
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
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
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 _('Birth: %s') % patient.get_formatted_dob(format = '%Y %b %d %H:%M', honor_estimation = True),
517 'True'
518 ))
519
520 # start of care
521 timeline.write(__xml_encounter_template % (
522 format_pydt(earliest_care_date),
523 format_pydt(earliest_care_date),
524 gmTools.u_heavy_greek_cross,
525 _('Life events'),
526 _('Start of Care: %s\n(the earliest recorded event of care in this praxis)') % format_pydt(earliest_care_date, format = '%Y %b %d'),
527 'True'
528 ))
529
530 # containers must be defined before their
531 # subevents, so put health issues first
532 timeline.write('\n <!-- ========================================\n Health issues\n======================================== -->')
533 for issue in emr.health_issues:
534 timeline.write(__format_health_issue_as_timeline_xml(issue, patient, emr))
535
536 timeline.write('\n <!-- ========================================\n Episodes\n======================================== -->')
537 for epi in emr.get_episodes(order_by = 'pk_health_issue'):
538 timeline.write(__format_episode_as_timeline_xml(epi, patient))
539
540 if include_encounters:
541 timeline.write(u'\n<!--\n========================================\n Encounters\n======================================== -->')
542 for enc in emr.get_encounters(skip_empty = True):
543 timeline.write(__format_encounter_as_timeline_xml(enc, patient))
544
545 timeline.write('\n<!--\n========================================\n Hospital stays\n======================================== -->')
546 for stay in emr.hospital_stays:
547 timeline.write(__format_hospital_stay_as_timeline_xml(stay))
548
549 timeline.write('\n<!--\n========================================\n Procedures\n======================================== -->')
550 for proc in emr.performed_procedures:
551 timeline.write(__format_procedure_as_timeline_xml(proc))
552
553 if include_vaccinations:
554 timeline.write(u'\n<!--\n========================================\n Vaccinations\n======================================== -->')
555 for vacc in emr.vaccinations:
556 timeline.write(__format_vaccination_as_timeline_xml(vacc))
557
558 timeline.write('\n<!--\n========================================\n Substance intakes\n======================================== -->')
559 for intake in emr.get_current_medications(include_inactive = True, include_unapproved = False):
560 timeline.write(__format_intake_as_timeline_xml(intake))
561
562 if include_documents:
563 timeline.write(u'\n<!--\n========================================\n Documents\n======================================== -->')
564 for doc in patient.document_folder.documents:
565 timeline.write(__format_document_as_timeline_xml(doc))
566
567 # allergies ?
568 # - unclear where and how to place
569 # test results ?
570 # - too many events, at most "day sample drawn"
571
572 # death
573 if patient['deceased'] is None:
574 end = now
575 else:
576 end = patient['deceased']
577 death_ago = gmDateTime.format_apparent_age_medically (
578 age = gmDateTime.calculate_apparent_age(start = end, end = now)
579 )
580 timeline.write(__xml_encounter_template % (
581 format_pydt(end),
582 format_pydt(end),
583 gmTools.u_dagger,
584 _('Life events'),
585 _('Death: %s\n(%s ago at age %s)') % (
586 format_pydt(end, format = '%Y %b %d %H:%M'),
587 death_ago,
588 patient.get_medical_age()
589 ),
590 'True'
591 ))
592
593 # display range
594 if end.month == 2:
595 if end.day == 29:
596 # leap years aren't consecutive
597 end = end.replace(day = 28)
598 target_year = end.year + 1
599 end = end.replace(year = target_year)
600 timeline.write(xml_end % (
601 format_pydt(start),
602 format_pydt(end)
603 ))
604
605 timeline.close()
606 return timeline_fname
607
608 #------------------------------------------------------------
609 __fake_timeline_start = """<?xml version="1.0" encoding="utf-8"?>
610 <timeline>
611 <version>0.20.0</version>
612 <categories>
613 <!-- life events -->
614 <category>
615 <name>%s</name>
616 <color>30,144,255</color>
617 <font_color>0,0,0</font_color>
618 </category>
619 </categories>
620 <events>""" % _('Life events')
621
622 __fake_timeline_body_template = """
623 <event>
624 <start>%s</start>
625 <end>%s</end>
626 <text>%s</text>
627 <fuzzy>False</fuzzy>
628 <locked>True</locked>
629 <ends_today>False</ends_today>
630 <!-- category></category -->
631 <description>%s
632 </description>
633 </event>"""
634
636 """Used to create an 'empty' timeline file for display.
637
638 - needed because .clear_timeline() doesn't really work
639 """
640 emr = patient.emr
641 global now
642 now = gmDateTime.pydt_now_here()
643
644 if filename is None:
645 timeline_fname = gmTools.get_unique_filename(prefix = 'gm-', suffix = '.timeline')
646 else:
647 timeline_fname = filename
648
649 _log.debug('creating dummy timeline in [%s]', timeline_fname)
650 timeline = io.open(timeline_fname, mode = 'wt', encoding = 'utf8', errors = 'xmlcharrefreplace')
651
652 timeline.write(__fake_timeline_start)
653
654 # birth
655 if patient['dob'] is None:
656 start = now.replace(year = now.year - 100)
657 timeline.write(__xml_encounter_template % (
658 format_pydt(start),
659 format_pydt(start),
660 _('Birth') + ': ?',
661 _('Life events'),
662 _('Date of birth unknown'),
663 'False'
664 ))
665 else:
666 start = patient['dob']
667 timeline.write(__xml_encounter_template % (
668 format_pydt(patient['dob']),
669 format_pydt(patient['dob']),
670 _('Birth') + gmTools.bool2subst(patient['dob_is_estimated'], ' (%s)' % gmTools.u_almost_equal_to, ''),
671 _('Life events'),
672 '',
673 'False'
674 ))
675
676 # death
677 if patient['deceased'] is None:
678 end = now
679 else:
680 end = patient['deceased']
681 timeline.write(__xml_encounter_template % (
682 format_pydt(end),
683 format_pydt(end),
684 #u'',
685 _('Death'),
686 _('Life events'),
687 '',
688 'False'
689 ))
690
691 # fake issue
692 timeline.write(__fake_timeline_body_template % (
693 format_pydt(start),
694 format_pydt(end),
695 _('Cannot display timeline.'),
696 _('Cannot display timeline.')
697 ))
698
699 # display range
700 if end.month == 2:
701 if end.day == 29:
702 # leap years aren't consecutive
703 end = end.replace(day = 28)
704 target_year = end.year + 1
705 end = end.replace(year = target_year)
706 timeline.write(xml_end % (
707 format_pydt(start),
708 format_pydt(end)
709 ))
710
711 timeline.close()
712 return timeline_fname
713
714 #============================================================
715 # main
716 #------------------------------------------------------------
717 if __name__ == '__main__':
718
719 if len(sys.argv) < 2:
720 sys.exit()
721
722 if sys.argv[1] != "test":
723 sys.exit()
724
725 gmI18N.activate_locale()
726 gmI18N.install_domain('gnumed')
727
728 from Gnumed.business import gmPraxis
729 praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0])
730
731 from Gnumed.business import gmPerson
732 # 14 / 20 / 138 / 58 / 20 / 5
733 pat = gmPerson.gmCurrentPatient(gmPerson.cPatient(aPK_obj = 14))
734 fname = '~/gnumed/gm2tl-%s.timeline' % pat.subdir_name
735
736 print (create_timeline_file (
737 patient = pat,
738 filename = os.path.expanduser(fname),
739 include_documents = True,
740 include_vaccinations = True,
741 include_encounters = True
742 ))
743
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sat Feb 29 02:55:27 2020 | http://epydoc.sourceforge.net |