| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2 """GNUmed patient objects.
3
4 This is a patient object intended to let a useful client-side
5 API crystallize from actual use in true XP fashion.
6 """
7 #============================================================
8 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
9 __license__ = "GPL"
10
11 # std lib
12 import sys
13 import os.path
14 import time
15 import re as regex
16 import datetime as pyDT
17 import io
18 import threading
19 import logging
20 import io
21 import inspect
22 from xml.etree import ElementTree as etree
23
24
25 # GNUmed
26 if __name__ == '__main__':
27 logging.basicConfig(level = logging.DEBUG)
28 sys.path.insert(0, '../../')
29 from Gnumed.pycommon import gmExceptions
30 from Gnumed.pycommon import gmDispatcher
31 from Gnumed.pycommon import gmBorg
32 from Gnumed.pycommon import gmI18N
33 if __name__ == '__main__':
34 gmI18N.activate_locale()
35 gmI18N.install_domain()
36 from Gnumed.pycommon import gmNull
37 from Gnumed.pycommon import gmBusinessDBObject
38 from Gnumed.pycommon import gmTools
39 from Gnumed.pycommon import gmPG2
40 from Gnumed.pycommon import gmDateTime
41 from Gnumed.pycommon import gmMatchProvider
42 from Gnumed.pycommon import gmLog2
43 from Gnumed.pycommon import gmHooks
44
45 from Gnumed.business import gmDemographicRecord
46 from Gnumed.business import gmClinicalRecord
47 from Gnumed.business import gmXdtMappings
48 from Gnumed.business import gmProviderInbox
49 from Gnumed.business import gmExportArea
50 from Gnumed.business import gmBilling
51 from Gnumed.business import gmAutoHints
52 from Gnumed.business.gmDocuments import cDocumentFolder
53
54
55 _log = logging.getLogger('gm.person')
56
57 __gender_list = None
58 __gender_idx = None
59
60 __gender2salutation_map = None
61 __gender2string_map = None
62
63 #============================================================
64 _MERGE_SCRIPT_HEADER = """-- GNUmed patient merge script
65 -- created: %(date)s
66 -- patient to keep : #%(pat2keep)s
67 -- patient to merge: #%(pat2del)s
68 --
69 -- You can EASILY cause mangled data by uncritically applying this script, so ...
70 -- ... BE POSITIVELY SURE YOU UNDERSTAND THE FULL EXTENT OF WHAT IT DOES !
71
72
73 --set default_transaction_read_only to off;
74
75 BEGIN;
76 """
77
78 #============================================================
80 cmd = 'SELECT COUNT(1) FROM dem.lnk_identity2ext_id WHERE fk_origin = %(issuer)s AND external_id = %(val)s'
81 args = {'issuer': pk_issuer, 'val': value}
82 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
83 return rows[0][0]
84
85 #============================================================
87 args = {
88 'last': lastnames,
89 'dob': dob
90 }
91 where_parts = [
92 "lastnames = %(last)s",
93 "dem.date_trunc_utc('day', dob) = dem.date_trunc_utc('day', %(dob)s)"
94 ]
95 if firstnames is not None:
96 if firstnames.strip() != '':
97 #where_parts.append(u"position(%(first)s in firstnames) = 1")
98 where_parts.append("firstnames ~* %(first)s")
99 args['first'] = '\\m' + firstnames
100 if active_only:
101 cmd = """SELECT COUNT(1) FROM dem.v_active_persons WHERE %s""" % ' AND '.join(where_parts)
102 else:
103 cmd = """SELECT COUNT(1) FROM dem.v_all_persons WHERE %s""" % ' AND '.join(where_parts)
104 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
105 return rows[0][0]
106
107 #============================================================
109 # backend also looks at gender (IOW, only fails on same-gender dupes)
110 # implement in plpgsql and re-use in both validation trigger and here
111 if comment is not None:
112 comment = comment.strip()
113 if comment == u'':
114 comment = None
115 args = {
116 'last': lastnames.strip(),
117 'first': firstnames.strip(),
118 'dob': dob,
119 'cmt': comment
120 }
121 where_parts = [
122 u'lower(lastnames) = lower(%(last)s)',
123 u'lower(firstnames) = lower(%(first)s)',
124 u"dem.date_trunc_utc('day', dob) IS NOT DISTINCT FROM dem.date_trunc_utc('day', %(dob)s)",
125 u'lower(comment) IS NOT DISTINCT FROM lower(%(cmt)s)'
126 ]
127 cmd = u"SELECT COUNT(1) FROM dem.v_persons WHERE %s" % u' AND '.join(where_parts)
128 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
129 return rows[0][0]
130
131 #============================================================
132 # FIXME: make this work as a mapping type, too
134
136 self.identity = None
137 self.external_ids = []
138 self.comm_channels = []
139 self.addresses = []
140
141 self.firstnames = None
142 self.lastnames = None
143 self.title = None
144 self.gender = None
145 self.dob = None
146 self.dob_is_estimated = False
147 self.source = self.__class__.__name__
148 #--------------------------------------------------------
149 # external API
150 #--------------------------------------------------------
153 #--------------------------------------------------------
156 #--------------------------------------------------------
158 where_snippets = [
159 'firstnames = %(first)s',
160 'lastnames = %(last)s'
161 ]
162 args = {
163 'first': self.firstnames,
164 'last': self.lastnames
165 }
166 if self.dob is not None:
167 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
168 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
169 if self.gender is not None:
170 where_snippets.append('gender = %(sex)s')
171 args['sex'] = self.gender
172 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
173 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
174
175 return rows[0][0] == 1
176
177 is_unique = property(is_unique, lambda x:x)
178 #--------------------------------------------------------
180 where_snippets = [
181 'firstnames = %(first)s',
182 'lastnames = %(last)s'
183 ]
184 args = {
185 'first': self.firstnames,
186 'last': self.lastnames
187 }
188 if self.dob is not None:
189 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
190 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
191 if self.gender is not None:
192 where_snippets.append('gender = %(sex)s')
193 args['sex'] = self.gender
194 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
195 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
196
197 return rows[0][0] > 0
198
199 exists = property(exists, lambda x:x)
200 #--------------------------------------------------------
202 """Generate generic queries.
203
204 - not locale dependant
205 - data -> firstnames, lastnames, dob, gender
206
207 shall we mogrify name parts ? probably not as external
208 sources should know what they do
209
210 finds by inactive name, too, but then shows
211 the corresponding active name ;-)
212
213 Returns list of matching identities (may be empty)
214 or None if it was told to create an identity but couldn't.
215 """
216 where_snippets = []
217 args = {}
218
219 where_snippets.append('lower(firstnames) = lower(%(first)s)')
220 args['first'] = self.firstnames
221
222 where_snippets.append('lower(lastnames) = lower(%(last)s)')
223 args['last'] = self.lastnames
224
225 if self.dob is not None:
226 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
227 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
228
229 if self.gender is not None:
230 where_snippets.append('lower(gender) = lower(%(sex)s)')
231 args['sex'] = self.gender
232
233 # FIXME: allow disabled persons ?
234 cmd = """
235 SELECT *, '%s' AS match_type
236 FROM dem.v_active_persons
237 WHERE
238 pk_identity IN (
239 SELECT pk_identity FROM dem.v_person_names WHERE %s
240 )
241 ORDER BY lastnames, firstnames, dob""" % (
242 _('external patient source (name, gender, date of birth)'),
243 ' AND '.join(where_snippets)
244 )
245
246 try:
247 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
248 except:
249 _log.error('cannot get candidate identities for dto "%s"' % self)
250 _log.exception('query %s' % cmd)
251 rows = []
252
253 if len(rows) == 0:
254 _log.debug('no candidate identity matches found')
255 if not can_create:
256 return []
257 ident = self.import_into_database()
258 if ident is None:
259 return None
260 identities = [ident]
261 else:
262 identities = [ cPerson(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
263
264 return identities
265 #--------------------------------------------------------
267 """Imports self into the database."""
268
269 self.identity = create_identity (
270 firstnames = self.firstnames,
271 lastnames = self.lastnames,
272 gender = self.gender,
273 dob = self.dob
274 )
275
276 if self.identity is None:
277 return None
278
279 if self.dob_is_estimated:
280 self.identity['dob_is_estimated'] = True
281 if self.title is not None:
282 self.identity['title'] = self.title
283 self.identity.save()
284
285 for ext_id in self.external_ids:
286 try:
287 self.identity.add_external_id (
288 type_name = ext_id['name'],
289 value = ext_id['value'],
290 issuer = ext_id['issuer'],
291 comment = ext_id['comment']
292 )
293 except Exception:
294 _log.exception('cannot import <external ID> from external data source')
295 gmLog2.log_stack_trace()
296
297 for comm in self.comm_channels:
298 try:
299 self.identity.link_comm_channel (
300 comm_medium = comm['channel'],
301 url = comm['url']
302 )
303 except Exception:
304 _log.exception('cannot import <comm channel> from external data source')
305 gmLog2.log_stack_trace()
306
307 for adr in self.addresses:
308 try:
309 self.identity.link_address (
310 adr_type = adr['type'],
311 number = adr['number'],
312 subunit = adr['subunit'],
313 street = adr['street'],
314 postcode = adr['zip'],
315 urb = adr['urb'],
316 region_code = adr['region_code'],
317 country_code = adr['country_code']
318 )
319 except Exception:
320 _log.exception('cannot import <address> from external data source')
321 gmLog2.log_stack_trace()
322
323 return self.identity
324 #--------------------------------------------------------
327 #--------------------------------------------------------
329 value = value.strip()
330 if value == '':
331 return
332 name = name.strip()
333 if name == '':
334 raise ValueError(_('<name> cannot be empty'))
335 issuer = issuer.strip()
336 if issuer == '':
337 raise ValueError(_('<issuer> cannot be empty'))
338 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
339 #--------------------------------------------------------
341 url = url.strip()
342 if url == '':
343 return
344 channel = channel.strip()
345 if channel == '':
346 raise ValueError(_('<channel> cannot be empty'))
347 self.comm_channels.append({'channel': channel, 'url': url})
348 #--------------------------------------------------------
349 - def remember_address(self, number=None, street=None, urb=None, region_code=None, zip=None, country_code=None, adr_type=None, subunit=None):
350 number = number.strip()
351 if number == '':
352 raise ValueError(_('<number> cannot be empty'))
353 street = street.strip()
354 if street == '':
355 raise ValueError(_('<street> cannot be empty'))
356 urb = urb.strip()
357 if urb == '':
358 raise ValueError(_('<urb> cannot be empty'))
359 zip = zip.strip()
360 if zip == '':
361 raise ValueError(_('<zip> cannot be empty'))
362 country_code = country_code.strip()
363 if country_code == '':
364 raise ValueError(_('<country_code> cannot be empty'))
365 if region_code is not None:
366 region_code = region_code.strip()
367 if region_code in [None, '']:
368 region_code = '??'
369 self.addresses.append ({
370 'type': adr_type,
371 'number': number,
372 'subunit': subunit,
373 'street': street,
374 'zip': zip,
375 'urb': urb,
376 'region_code': region_code,
377 'country_code': country_code
378 })
379 #--------------------------------------------------------
380 # customizing behaviour
381 #--------------------------------------------------------
383 return '<%s (%s) @ %s: %s %s (%s) %s>' % (
384 self.__class__.__name__,
385 self.source,
386 id(self),
387 self.firstnames,
388 self.lastnames,
389 self.gender,
390 self.dob
391 )
392 #--------------------------------------------------------
394 """Do some sanity checks on self.* access."""
395
396 if attr == 'gender':
397 if val is None:
398 object.__setattr__(self, attr, val)
399 return
400 glist, idx = get_gender_list()
401 for gender in glist:
402 if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
403 val = gender[idx['tag']]
404 object.__setattr__(self, attr, val)
405 return
406 raise ValueError('invalid gender: [%s]' % val)
407
408 if attr == 'dob':
409 if val is not None:
410 if not isinstance(val, pyDT.datetime):
411 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
412 if val.tzinfo is None:
413 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
414
415 object.__setattr__(self, attr, val)
416 return
417 #--------------------------------------------------------
420
421 #============================================================
423 _cmd_fetch_payload = "SELECT * FROM dem.v_person_names WHERE pk_name = %s"
424 _cmds_store_payload = [
425 """UPDATE dem.names SET
426 active = FALSE
427 WHERE
428 %(active_name)s IS TRUE -- act only when needed and only
429 AND
430 id_identity = %(pk_identity)s -- on names of this identity
431 AND
432 active IS TRUE -- which are active
433 AND
434 id != %(pk_name)s -- but NOT *this* name
435 """,
436 """update dem.names set
437 active = %(active_name)s,
438 preferred = %(preferred)s,
439 comment = %(comment)s
440 where
441 id = %(pk_name)s and
442 id_identity = %(pk_identity)s and -- belt and suspenders
443 xmin = %(xmin_name)s""",
444 """select xmin as xmin_name from dem.names where id = %(pk_name)s"""
445 ]
446 _updatable_fields = ['active_name', 'preferred', 'comment']
447 #--------------------------------------------------------
449 if attribute == 'active_name':
450 # cannot *directly* deactivate a name, only indirectly
451 # by activating another one
452 # FIXME: should be done at DB level
453 if self._payload[self._idx['active_name']] is True:
454 return
455 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
456 #--------------------------------------------------------
458 return '%(last)s, %(title)s %(first)s%(nick)s' % {
459 'last': self._payload[self._idx['lastnames']],
460 'title': gmTools.coalesce (
461 self._payload[self._idx['title']],
462 map_gender2salutation(self._payload[self._idx['gender']])
463 ),
464 'first': self._payload[self._idx['firstnames']],
465 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'", '%s')
466 }
467
468 description = property(_get_description, lambda x:x)
469
470 #============================================================
471 _SQL_get_active_person = "SELECT * FROM dem.v_active_persons WHERE pk_identity = %s"
472 _SQL_get_any_person = "SELECT * FROM dem.v_all_persons WHERE pk_identity = %s"
473
475 _cmd_fetch_payload = _SQL_get_any_person
476 _cmds_store_payload = [
477 """UPDATE dem.identity SET
478 gender = %(gender)s,
479 dob = %(dob)s,
480 dob_is_estimated = %(dob_is_estimated)s,
481 tob = %(tob)s,
482 title = gm.nullify_empty_string(%(title)s),
483 fk_marital_status = %(pk_marital_status)s,
484 deceased = %(deceased)s,
485 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
486 fk_emergency_contact = %(pk_emergency_contact)s,
487 fk_primary_provider = %(pk_primary_provider)s,
488 comment = gm.nullify_empty_string(%(comment)s)
489 WHERE
490 pk = %(pk_identity)s and
491 xmin = %(xmin_identity)s
492 RETURNING
493 xmin AS xmin_identity"""
494 ]
495 _updatable_fields = [
496 "title",
497 "dob",
498 "tob",
499 "gender",
500 "pk_marital_status",
501 'deceased',
502 'emergency_contact',
503 'pk_emergency_contact',
504 'pk_primary_provider',
505 'comment',
506 'dob_is_estimated'
507 ]
508 #--------------------------------------------------------
513
514 ID = property(_get_ID, _set_ID)
515
516 #--------------------------------------------------------
518
519 if attribute == 'dob':
520 if value is not None:
521
522 if isinstance(value, pyDT.datetime):
523 if value.tzinfo is None:
524 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
525 else:
526 raise TypeError('[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value))
527
528 # compare DOB at seconds level
529 if self._payload[self._idx['dob']] is not None:
530 old_dob = gmDateTime.pydt_strftime (
531 self._payload[self._idx['dob']],
532 format = '%Y %m %d %H %M %S',
533 accuracy = gmDateTime.acc_seconds
534 )
535 new_dob = gmDateTime.pydt_strftime (
536 value,
537 format = '%Y %m %d %H %M %S',
538 accuracy = gmDateTime.acc_seconds
539 )
540 if new_dob == old_dob:
541 return
542
543 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
544
545 #--------------------------------------------------------
548
549 #--------------------------------------------------------
551 return identity_is_patient(self._payload[self._idx['pk_identity']])
552
554 if turn_into_patient:
555 return turn_identity_into_patient(self._payload[self._idx['pk_identity']])
556 return False
557
558 is_patient = property(_get_is_patient, _set_is_patient)
559
560 #--------------------------------------------------------
562 return cPatient(self._payload[self._idx['pk_identity']])
563
564 as_patient = property(_get_as_patient, lambda x:x)
565
566 #--------------------------------------------------------
568 cmd = "SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
569 args = {'pk': self._payload[self._idx['pk_identity']]}
570 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
571 if len(rows) == 0:
572 return None
573 return rows[0][0]
574
575 staff_id = property(_get_staff_id, lambda x:x)
576
577 #--------------------------------------------------------
578 # identity API
579 #--------------------------------------------------------
581 return map_gender2symbol[self._payload[self._idx['gender']]]
582
583 gender_symbol = property(_get_gender_symbol, lambda x:x)
584 #--------------------------------------------------------
587
588 gender_string = property(_get_gender_string, lambda x:x)
589 #--------------------------------------------------------
593
594 gender_list = property(_get_gender_list, lambda x:x)
595 #--------------------------------------------------------
597 names = self.get_names(active_only = True)
598 if len(names) == 0:
599 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
600 return None
601 return names[0]
602
603 active_name = property(get_active_name, lambda x:x)
604 #--------------------------------------------------------
606
607 args = {'pk_pat': self._payload[self._idx['pk_identity']]}
608 where_parts = ['pk_identity = %(pk_pat)s']
609 if active_only:
610 where_parts.append('active_name is True')
611 if exclude_active:
612 where_parts.append('active_name is False')
613 cmd = """
614 SELECT *
615 FROM dem.v_person_names
616 WHERE %s
617 ORDER BY active_name DESC, lastnames, firstnames
618 """ % ' AND '.join(where_parts)
619 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
620
621 if len(rows) == 0:
622 # no names registered for patient
623 return []
624
625 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
626 return names
627 #--------------------------------------------------------
629 if with_nickname:
630 template = _('%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)')
631 else:
632 template = _('%(last)s,%(title)s %(first)s (%(sex)s)')
633 return template % {
634 'last': self._payload[self._idx['lastnames']],
635 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
636 'first': self._payload[self._idx['firstnames']],
637 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'"),
638 'sex': self.gender_symbol
639 }
640
641 #--------------------------------------------------------
643 if with_nickname:
644 template = _('%(last)s,%(title)s %(first)s%(nick)s')
645 else:
646 template = _('%(last)s,%(title)s %(first)s')
647 return template % {
648 'last': self._payload[self._idx['lastnames']],
649 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
650 'first': self._payload[self._idx['firstnames']],
651 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'")
652 }
653
654 #--------------------------------------------------------
656 """Add a name.
657
658 @param firstnames The first names.
659 @param lastnames The last names.
660 @param active When True, the new name will become the active one (hence setting other names to inactive)
661 @type active A bool instance
662 """
663 name = create_name(self.ID, firstnames, lastnames, active)
664 if active:
665 self.refetch_payload()
666 return name
667
668 #--------------------------------------------------------
670 cmd = "delete from dem.names where id = %(name)s and id_identity = %(pat)s"
671 args = {'name': name['pk_name'], 'pat': self.ID}
672 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
673 # can't have been the active name as that would raise an
674 # exception (since no active name would be left) so no
675 # data refetch needed
676
677 #--------------------------------------------------------
679 """
680 Set the nickname. Setting the nickname only makes sense for the currently
681 active name.
682 @param nickname The preferred/nick/warrior name to set.
683 """
684 if self._payload[self._idx['preferred']] == nickname:
685 return True
686 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': "SELECT dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
687 # setting nickname doesn't change dem.identity, so other fields
688 # of dem.v_active_persons do not get changed as a consequence of
689 # setting the nickname, hence locally setting nickname matches
690 # in-database reality
691 self._payload[self._idx['preferred']] = nickname
692 #self.refetch_payload()
693 return True
694
695 #--------------------------------------------------------
706
707 tags = property(get_tags, lambda x:x)
708
709 #--------------------------------------------------------
711 args = {
712 'tag': tag,
713 'identity': self.ID
714 }
715
716 # already exists ?
717 cmd = "SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
718 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
719 if len(rows) > 0:
720 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
721
722 # no, add
723 cmd = """
724 INSERT INTO dem.identity_tag (
725 fk_tag,
726 fk_identity
727 ) VALUES (
728 %(tag)s,
729 %(identity)s
730 )
731 RETURNING pk
732 """
733 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
734 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
735
736 #--------------------------------------------------------
738 cmd = "DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
739 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
740
741 #--------------------------------------------------------
742 # external ID API
743 #
744 # since external IDs are not treated as first class
745 # citizens (classes in their own right, that is), we
746 # handle them *entirely* within cPerson, also they
747 # only make sense with one single person (like names)
748 # and are not reused (like addresses), so they are
749 # truly added/deleted, not just linked/unlinked
750 #--------------------------------------------------------
751 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
752 """Adds an external ID to the patient.
753
754 creates ID type if necessary
755 """
756 # check for existing ID
757 if pk_type is not None:
758 cmd = """
759 select * from dem.v_external_ids4identity where
760 pk_identity = %(pat)s and
761 pk_type = %(pk_type)s and
762 value = %(val)s"""
763 else:
764 # by type/value/issuer
765 if issuer is None:
766 cmd = """
767 select * from dem.v_external_ids4identity where
768 pk_identity = %(pat)s and
769 name = %(name)s and
770 value = %(val)s"""
771 else:
772 cmd = """
773 select * from dem.v_external_ids4identity where
774 pk_identity = %(pat)s and
775 name = %(name)s and
776 value = %(val)s and
777 issuer = %(issuer)s"""
778 args = {
779 'pat': self.ID,
780 'name': type_name,
781 'val': value,
782 'issuer': issuer,
783 'pk_type': pk_type
784 }
785 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
786
787 # create new ID if not found
788 if len(rows) == 0:
789
790 args = {
791 'pat': self.ID,
792 'val': value,
793 'type_name': type_name,
794 'pk_type': pk_type,
795 'issuer': issuer,
796 'comment': comment
797 }
798
799 if pk_type is None:
800 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
801 %(val)s,
802 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
803 %(comment)s,
804 %(pat)s
805 )"""
806 else:
807 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
808 %(val)s,
809 %(pk_type)s,
810 %(comment)s,
811 %(pat)s
812 )"""
813
814 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
815
816 # or update comment of existing ID
817 else:
818 row = rows[0]
819 if comment is not None:
820 # comment not already there ?
821 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
822 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
823 cmd = "update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
824 args = {'comment': comment, 'pk': row['pk_id']}
825 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
826
827 #--------------------------------------------------------
829 """Edits an existing external ID.
830
831 Creates ID type if necessary.
832 """
833 cmd = """
834 UPDATE dem.lnk_identity2ext_id SET
835 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
836 external_id = %(value)s,
837 comment = gm.nullify_empty_string(%(comment)s)
838 WHERE
839 id = %(pk)s
840 """
841 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
842 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
843
844 #--------------------------------------------------------
846 where_parts = ['pk_identity = %(pat)s']
847 args = {'pat': self.ID}
848
849 if id_type is not None:
850 where_parts.append('name = %(name)s')
851 args['name'] = id_type.strip()
852
853 if issuer is not None:
854 where_parts.append('issuer = %(issuer)s')
855 args['issuer'] = issuer.strip()
856
857 cmd = "SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
858 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
859
860 return rows
861
862 external_ids = property(get_external_ids, lambda x:x)
863
864 #--------------------------------------------------------
866 cmd = """
867 DELETE FROM dem.lnk_identity2ext_id
868 WHERE id_identity = %(pat)s AND id = %(pk)s"""
869 args = {'pat': self.ID, 'pk': pk_ext_id}
870 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
871
872 #--------------------------------------------------------
874 name = self.active_name
875 last = ' '.join(p for p in name['lastnames'].split("-"))
876 last = ' '.join(p for p in last.split("."))
877 last = ' '.join(p for p in last.split("'"))
878 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
879 first = ' '.join(p for p in name['firstnames'].split("-"))
880 first = ' '.join(p for p in first.split("."))
881 first = ' '.join(p for p in first.split("'"))
882 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
883 suggestion = 'GMd-%s%s%s%s%s' % (
884 gmTools.coalesce(target, '', '%s-'),
885 last,
886 first,
887 self.get_formatted_dob(format = '-%Y%m%d', none_string = ''),
888 gmTools.coalesce(self['gender'], '', '-%s')
889 )
890 try:
891 import unidecode
892 return unidecode.unidecode(suggestion)
893 except ImportError:
894 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
895 if encoding is None:
896 return suggestion
897 return suggestion.encode(encoding)
898
899 external_id_suggestion = property(suggest_external_id, lambda x:x)
900
901 #--------------------------------------------------------
903 names2use = [self.active_name]
904 names2use.extend(self.get_names(active_only = False, exclude_active = True))
905 target = gmTools.coalesce(target, '', '%s-')
906 dob = self.get_formatted_dob(format = '-%Y%m%d', none_string = '')
907 gender = gmTools.coalesce(self['gender'], '', '-%s')
908 suggestions = []
909 for name in names2use:
910 last = ' '.join(p for p in name['lastnames'].split("-"))
911 last = ' '.join(p for p in last.split("."))
912 last = ' '.join(p for p in last.split("'"))
913 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
914 first = ' '.join(p for p in name['firstnames'].split("-"))
915 first = ' '.join(p for p in first.split("."))
916 first = ' '.join(p for p in first.split("'"))
917 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
918 suggestion = 'GMd-%s%s%s%s%s' % (target, last, first, dob, gender)
919 try:
920 import unidecode
921 suggestions.append(unidecode.unidecode(suggestion))
922 continue
923 except ImportError:
924 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
925 if encoding is None:
926 suggestions.append(suggestion)
927 else:
928 suggestions.append(suggestion.encode(encoding))
929 return suggestions
930
931 #--------------------------------------------------------
932 #--------------------------------------------------------
934 """Merge another identity into this one.
935
936 Keep this one. Delete other one."""
937
938 if other_identity.ID == self.ID:
939 return True, None
940
941 curr_pat = gmCurrentPatient()
942 if curr_pat.connected:
943 if other_identity.ID == curr_pat.ID:
944 return False, _('Cannot merge active patient into another patient.')
945
946 now_here = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())
947 distinguisher = _('merge of #%s into #%s @ %s') % (other_identity.ID, self.ID, now_here)
948
949 queries = []
950 args = {'pat2del': other_identity.ID, 'pat2keep': self.ID}
951
952 # merge allergy state
953 queries.append ({
954 'cmd': """
955 UPDATE clin.allergy_state SET
956 has_allergy = greatest (
957 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
958 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
959 ),
960 -- perhaps use least() to play it safe and make it appear longer ago than it might have been, actually ?
961 last_confirmed = greatest (
962 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
963 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
964 )
965 WHERE
966 pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
967 """,
968 'args': args
969 })
970 # delete old allergy state
971 queries.append ({
972 'cmd': 'DELETE FROM clin.allergy_state WHERE pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s)',
973 'args': args
974 })
975
976 # merge patient proxy
977 queries.append ({
978 'cmd': """
979 UPDATE clin.patient SET
980 edc = coalesce (
981 edc,
982 (SELECT edc FROM clin.patient WHERE fk_identity = %(pat2del)s)
983 )
984 WHERE
985 fk_identity = %(pat2keep)s
986 """,
987 'args': args
988 })
989
990 # transfer names
991 # 1) hard-disambiguate all inactive names in old patient
992 # (the active one will be disambiguated upon being moved)
993 queries.append ({
994 'cmd': """
995 UPDATE dem.names d_n1 SET
996 comment = coalesce (
997 comment, ''
998 ) || coalesce (
999 ' (from identity: "' || (SELECT comment FROM dem.identity WHERE pk = %%(pat2del)s) || '")',
1000 ''
1001 ) || ' (during: "%s")'
1002 WHERE
1003 d_n1.id_identity = %%(pat2del)s
1004 """ % distinguisher,
1005 'args': args
1006 })
1007 # 2) move inactive ones (dupes are expected to have been eliminated in step 1 above)
1008 queries.append ({
1009 'cmd': u"""
1010 UPDATE dem.names d_n SET
1011 id_identity = %(pat2keep)s,
1012 lastnames = lastnames || ' [' || random()::TEXT || ']'
1013 WHERE
1014 d_n.id_identity = %(pat2del)s
1015 AND
1016 d_n.active IS false
1017 """,
1018 'args': args
1019 })
1020 # 3) copy active name into pat2keep as an inactive name,
1021 # because each identity MUST have at LEAST one name,
1022 # we can't simply UPDATE over to pat2keep
1023 # also, needs de-duplication or else it would conflict with
1024 # *itself* on pat2keep
1025 queries.append ({
1026 'cmd': """
1027 INSERT INTO dem.names (
1028 id_identity, active, firstnames, preferred, comment,
1029 lastnames
1030 )
1031 SELECT
1032 %(pat2keep)s, false, firstnames, preferred, comment,
1033 lastnames || ' [' || random()::text || ']'
1034 FROM dem.names d_n
1035 WHERE
1036 d_n.id_identity = %(pat2del)s
1037 AND
1038 d_n.active IS true
1039 """,
1040 'args': args
1041 })
1042
1043 # disambiguate potential dupes
1044 # - same-url comm channels
1045 queries.append ({
1046 'cmd': """
1047 UPDATE dem.lnk_identity2comm
1048 SET url = url || ' (%s)'
1049 WHERE
1050 fk_identity = %%(pat2del)s
1051 AND
1052 EXISTS (
1053 SELECT 1 FROM dem.lnk_identity2comm d_li2c
1054 WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url
1055 )
1056 """ % distinguisher,
1057 'args': args
1058 })
1059 # - same-value external IDs
1060 queries.append ({
1061 'cmd': """
1062 UPDATE dem.lnk_identity2ext_id
1063 SET external_id = external_id || ' (%s)'
1064 WHERE
1065 id_identity = %%(pat2del)s
1066 AND
1067 EXISTS (
1068 SELECT 1 FROM dem.lnk_identity2ext_id d_li2e
1069 WHERE
1070 d_li2e.id_identity = %%(pat2keep)s
1071 AND
1072 d_li2e.external_id = external_id
1073 AND
1074 d_li2e.fk_origin = fk_origin
1075 )
1076 """ % distinguisher,
1077 'args': args
1078 })
1079 # - same addresses
1080 queries.append ({
1081 'cmd': """
1082 DELETE FROM dem.lnk_person_org_address
1083 WHERE
1084 id_identity = %(pat2del)s
1085 AND
1086 id_address IN (
1087 SELECT id_address FROM dem.lnk_person_org_address d_lpoa
1088 WHERE d_lpoa.id_identity = %(pat2keep)s
1089 )
1090 """,
1091 'args': args
1092 })
1093
1094 # find FKs pointing to dem.identity.pk
1095 FKs = gmPG2.get_foreign_keys2column (
1096 schema = 'dem',
1097 table = 'identity',
1098 column = 'pk'
1099 )
1100 # find FKs pointing to clin.patient.fk_identity
1101 FKs.extend (gmPG2.get_foreign_keys2column (
1102 schema = 'clin',
1103 table = 'patient',
1104 column = 'fk_identity'
1105 ))
1106
1107 # generate UPDATEs
1108 cmd_template = 'UPDATE %s SET %s = %%(pat2keep)s WHERE %s = %%(pat2del)s'
1109 for FK in FKs:
1110 if FK['referencing_table'] in ['dem.names', 'clin.patient']:
1111 continue
1112 queries.append ({
1113 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
1114 'args': args
1115 })
1116
1117 # delete old patient proxy
1118 queries.append ({
1119 'cmd': 'DELETE FROM clin.patient WHERE fk_identity = %(pat2del)s',
1120 'args': args
1121 })
1122
1123 # remove old identity entry
1124 queries.append ({
1125 'cmd': 'delete from dem.identity where pk = %(pat2del)s',
1126 'args': args
1127 })
1128
1129 script_name = gmTools.get_unique_filename(prefix = 'gm-assimilate-%(pat2del)s-into-%(pat2keep)s-' % args, suffix = '.sql')
1130 _log.warning('identity [%s] is about to assimilate identity [%s], SQL script [%s]', self.ID, other_identity.ID, script_name)
1131
1132 script = io.open(script_name, 'wt')
1133 args['date'] = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %B %d %H:%M')
1134 script.write(_MERGE_SCRIPT_HEADER % args)
1135 for query in queries:
1136 script.write(query['cmd'] % args)
1137 script.write(';\n')
1138 script.write('\nROLLBACK;\n')
1139 script.write('--COMMIT;\n')
1140 script.close()
1141
1142 try:
1143 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
1144 except Exception:
1145 return False, _('The merge failed. Check the log and [%s]') % script_name
1146
1147 self.add_external_id (
1148 type_name = 'merged GNUmed identity primary key',
1149 value = 'GNUmed::pk::%s' % other_identity.ID,
1150 issuer = 'GNUmed'
1151 )
1152
1153 return True, None
1154
1155 #--------------------------------------------------------
1156 #--------------------------------------------------------
1158 cmd = """
1159 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
1160 values (
1161 %(pat)s,
1162 %(urg)s,
1163 %(cmt)s,
1164 %(area)s,
1165 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
1166 )"""
1167 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
1168 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
1169 gmHooks.run_hook_script(hook = 'after_waiting_list_modified')
1170
1171 #--------------------------------------------------------
1173 cmd = """SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
1174 args = {'pat': self.ID}
1175 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1176 return rows
1177
1178 waiting_list_entries = property(get_waiting_list_entry, lambda x:x)
1179
1180 #--------------------------------------------------------
1183
1184 export_area = property(_get_export_area, lambda x:x)
1185 #--------------------------------------------------------
1187
1188 template = '%s%s%s\r\n'
1189
1190 if filename is None:
1191 filename = gmTools.get_unique_filename (
1192 prefix = 'gm-patient-',
1193 suffix = '.gdt'
1194 )
1195
1196 gdt_file = io.open(filename, mode = 'wt', encoding = encoding, errors = 'strict')
1197
1198 gdt_file.write(template % ('013', '8000', '6301'))
1199 gdt_file.write(template % ('013', '9218', '2.10'))
1200 if external_id_type is None:
1201 gdt_file.write(template % ('%03d' % (9 + len(str(self.ID))), '3000', self.ID))
1202 else:
1203 ext_ids = self.get_external_ids(id_type = external_id_type)
1204 if len(ext_ids) > 0:
1205 gdt_file.write(template % ('%03d' % (9 + len(ext_ids[0]['value'])), '3000', ext_ids[0]['value']))
1206 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['lastnames']])), '3101', self._payload[self._idx['lastnames']]))
1207 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['firstnames']])), '3102', self._payload[self._idx['firstnames']]))
1208 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), '3103', self._payload[self._idx['dob']].strftime('%d%m%Y')))
1209 gdt_file.write(template % ('010', '3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
1210 gdt_file.write(template % ('025', '6330', 'GNUmed::9206::encoding'))
1211 gdt_file.write(template % ('%03d' % (9 + len(encoding)), '6331', encoding))
1212 if external_id_type is None:
1213 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
1214 gdt_file.write(template % ('017', '6333', 'internal'))
1215 else:
1216 if len(ext_ids) > 0:
1217 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
1218 gdt_file.write(template % ('%03d' % (9 + len(external_id_type)), '6333', external_id_type))
1219
1220 gdt_file.close()
1221
1222 return filename
1223 #--------------------------------------------------------
1225
1226 if filename is None:
1227 filename = gmTools.get_unique_filename (
1228 prefix = 'gm-LinuxMedNews_demographics-',
1229 suffix = '.xml'
1230 )
1231
1232 dob_format = '%Y-%m-%d'
1233 pat = etree.Element('patient')
1234
1235 first = etree.SubElement(pat, 'firstname')
1236 first.text = gmTools.coalesce(self._payload[self._idx['firstnames']], '')
1237
1238 last = etree.SubElement(pat, 'lastname')
1239 last.text = gmTools.coalesce(self._payload[self._idx['lastnames']], '')
1240
1241 # privacy
1242 #middle = etree.SubElement(pat, u'middlename')
1243 #middle.set(u'comment', _('preferred name/call name/...'))
1244 #middle.text = gmTools.coalesce(self._payload[self._idx['preferred']], u'')
1245
1246 pref = etree.SubElement(pat, 'name_prefix')
1247 pref.text = gmTools.coalesce(self._payload[self._idx['title']], '')
1248
1249 suff = etree.SubElement(pat, 'name_suffix')
1250 suff.text = ''
1251
1252 dob = etree.SubElement(pat, 'DOB')
1253 dob.set('format', dob_format)
1254 dob.text = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
1255
1256 gender = etree.SubElement(pat, 'gender')
1257 gender.set('comment', self.gender_string)
1258 if self._payload[self._idx['gender']] is None:
1259 gender.text = ''
1260 else:
1261 gender.text = map_gender2mf[self._payload[self._idx['gender']]]
1262
1263 home = etree.SubElement(pat, 'home_address')
1264 adrs = self.get_addresses(address_type = 'home')
1265 if len(adrs) > 0:
1266 adr = adrs[0]
1267 city = etree.SubElement(home, 'city')
1268 city.set('comment', gmTools.coalesce(adr['suburb'], ''))
1269 city.text = gmTools.coalesce(adr['urb'], '')
1270
1271 region = etree.SubElement(home, 'region')
1272 region.set('comment', gmTools.coalesce(adr['l10n_region'], ''))
1273 region.text = gmTools.coalesce(adr['code_region'], '')
1274
1275 zipcode = etree.SubElement(home, 'postal_code')
1276 zipcode.text = gmTools.coalesce(adr['postcode'], '')
1277
1278 street = etree.SubElement(home, 'street')
1279 street.set('comment', gmTools.coalesce(adr['notes_street'], ''))
1280 street.text = gmTools.coalesce(adr['street'], '')
1281
1282 no = etree.SubElement(home, 'number')
1283 no.set('subunit', gmTools.coalesce(adr['subunit'], ''))
1284 no.set('comment', gmTools.coalesce(adr['notes_subunit'], ''))
1285 no.text = gmTools.coalesce(adr['number'], '')
1286
1287 country = etree.SubElement(home, 'country')
1288 country.set('comment', adr['l10n_country'])
1289 country.text = gmTools.coalesce(adr['code_country'], '')
1290
1291 phone = etree.SubElement(pat, 'home_phone')
1292 rec = self.get_comm_channels(comm_medium = 'homephone')
1293 if len(rec) > 0:
1294 if not rec[0]['is_confidential']:
1295 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1296 phone.text = rec[0]['url']
1297
1298 phone = etree.SubElement(pat, 'work_phone')
1299 rec = self.get_comm_channels(comm_medium = 'workphone')
1300 if len(rec) > 0:
1301 if not rec[0]['is_confidential']:
1302 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1303 phone.text = rec[0]['url']
1304
1305 phone = etree.SubElement(pat, 'cell_phone')
1306 rec = self.get_comm_channels(comm_medium = 'mobile')
1307 if len(rec) > 0:
1308 if not rec[0]['is_confidential']:
1309 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1310 phone.text = rec[0]['url']
1311
1312 tree = etree.ElementTree(pat)
1313 tree.write(filename, encoding = 'UTF-8')
1314
1315 return filename
1316
1317 #--------------------------------------------------------
1319 # http://vobject.skyhouseconsulting.com/usage.html
1320 # http://en.wikipedia.org/wiki/VCard
1321 # http://svn.osafoundation.org/vobject/trunk/vobject/vcard.py
1322 # http://www.ietf.org/rfc/rfc2426.txt
1323
1324 dob_format = '%Y%m%d'
1325
1326 import vobject
1327
1328 vc = vobject.vCard()
1329 vc.add('kind')
1330 vc.kind.value = 'individual'
1331
1332 vc.add('fn')
1333 vc.fn.value = self.get_description()
1334 vc.add('n')
1335 vc.n.value = vobject.vcard.Name(family = self._payload[self._idx['lastnames']], given = self._payload[self._idx['firstnames']])
1336 # privacy
1337 #vc.add(u'nickname')
1338 #vc.nickname.value = gmTools.coalesce(self._payload[self._idx['preferred']], u'')
1339 vc.add('title')
1340 vc.title.value = gmTools.coalesce(self._payload[self._idx['title']], '')
1341 vc.add('gender')
1342 # FIXME: dont know how to add gender_string after ';'
1343 vc.gender.value = map_gender2vcard[self._payload[self._idx['gender']]]#, self.gender_string
1344 vc.add('bday')
1345 vc.bday.value = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
1346
1347 channels = self.get_comm_channels(comm_medium = 'homephone')
1348 if len(channels) > 0:
1349 if not channels[0]['is_confidential']:
1350 vc.add('tel')
1351 vc.tel.value = channels[0]['url']
1352 vc.tel.type_param = 'HOME'
1353 channels = self.get_comm_channels(comm_medium = 'workphone')
1354 if len(channels) > 0:
1355 if not channels[0]['is_confidential']:
1356 vc.add('tel')
1357 vc.tel.value = channels[0]['url']
1358 vc.tel.type_param = 'WORK'
1359 channels = self.get_comm_channels(comm_medium = 'mobile')
1360 if len(channels) > 0:
1361 if not channels[0]['is_confidential']:
1362 vc.add('tel')
1363 vc.tel.value = channels[0]['url']
1364 vc.tel.type_param = 'CELL'
1365 channels = self.get_comm_channels(comm_medium = 'fax')
1366 if len(channels) > 0:
1367 if not channels[0]['is_confidential']:
1368 vc.add('tel')
1369 vc.tel.value = channels[0]['url']
1370 vc.tel.type_param = 'FAX'
1371 channels = self.get_comm_channels(comm_medium = 'email')
1372 if len(channels) > 0:
1373 if not channels[0]['is_confidential']:
1374 vc.add('email')
1375 vc.tel.value = channels[0]['url']
1376 vc.tel.type_param = 'INTERNET'
1377 channels = self.get_comm_channels(comm_medium = 'web')
1378 if len(channels) > 0:
1379 if not channels[0]['is_confidential']:
1380 vc.add('url')
1381 vc.tel.value = channels[0]['url']
1382 vc.tel.type_param = 'INTERNET'
1383
1384 adrs = self.get_addresses(address_type = 'home')
1385 if len(adrs) > 0:
1386 home_adr = adrs[0]
1387 vc.add('adr')
1388 vc.adr.type_param = 'HOME'
1389 vc.adr.value = vobject.vcard.Address()
1390 vc_adr = vc.adr.value
1391 vc_adr.extended = gmTools.coalesce(home_adr['subunit'], '')
1392 vc_adr.street = gmTools.coalesce(home_adr['street'], '', '%s ') + gmTools.coalesce(home_adr['number'], '')
1393 vc_adr.region = gmTools.coalesce(home_adr['l10n_region'], '')
1394 vc_adr.code = gmTools.coalesce(home_adr['postcode'], '')
1395 vc_adr.city = gmTools.coalesce(home_adr['urb'], '')
1396 vc_adr.country = gmTools.coalesce(home_adr['l10n_country'], '')
1397
1398 #photo (base64)
1399
1400 if filename is None:
1401 filename = gmTools.get_unique_filename (
1402 prefix = 'gm-patient-',
1403 suffix = '.vcf'
1404 )
1405 vcf = io.open(filename, mode = 'wt', encoding = 'utf8')
1406 try:
1407 vcf.write(vc.serialize().decode('utf-8'))
1408 except UnicodeDecodeError:
1409 _log.exception('failed to serialize VCF data')
1410 vcf.close()
1411 return 'cannot-serialize.vcf'
1412 vcf.close()
1413
1414 return filename
1415 #--------------------------------------------------------
1416 # occupations API
1417 #--------------------------------------------------------
1420
1421 #--------------------------------------------------------
1423 """Link an occupation with a patient, creating the occupation if it does not exists.
1424
1425 @param occupation The name of the occupation to link the patient to.
1426 """
1427 if (activities is None) and (occupation is None):
1428 return True
1429
1430 occupation = occupation.strip()
1431 if len(occupation) == 0:
1432 return True
1433
1434 if activities is not None:
1435 activities = activities.strip()
1436
1437 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
1438
1439 cmd = "select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
1440 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1441
1442 queries = []
1443 if len(rows) == 0:
1444 queries.append ({
1445 'cmd': "INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
1446 'args': args
1447 })
1448 else:
1449 if rows[0]['activities'] != activities:
1450 queries.append ({
1451 'cmd': "update dem.lnk_job2person set activities=%(act)s where fk_identity=%(pat_id)s and fk_occupation=(select id from dem.occupation where _(name) = _(%(job)s))",
1452 'args': args
1453 })
1454
1455 rows, idx = gmPG2.run_rw_queries(queries = queries)
1456
1457 return True
1458 #--------------------------------------------------------
1460 if occupation is None:
1461 return True
1462 occupation = occupation.strip()
1463 cmd = "delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
1464 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
1465 return True
1466 #--------------------------------------------------------
1467 # comms API
1468 #--------------------------------------------------------
1470 cmd = "select * from dem.v_person_comms where pk_identity = %s"
1471 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
1472
1473 filtered = rows
1474
1475 if comm_medium is not None:
1476 filtered = []
1477 for row in rows:
1478 if row['comm_type'] == comm_medium:
1479 filtered.append(row)
1480
1481 return [ gmDemographicRecord.cCommChannel(row = {
1482 'pk_field': 'pk_lnk_identity2comm',
1483 'data': r,
1484 'idx': idx
1485 }) for r in filtered
1486 ]
1487
1488 comm_channels = property(get_comm_channels, lambda x:x)
1489 #--------------------------------------------------------
1490 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
1491 """Link a communication medium with a patient.
1492
1493 @param comm_medium The name of the communication medium.
1494 @param url The communication resource locator.
1495 @type url A str instance.
1496 @param is_confidential Wether the data must be treated as confidential.
1497 @type is_confidential A bool instance.
1498 """
1499 comm_channel = gmDemographicRecord.create_comm_channel (
1500 comm_medium = comm_medium,
1501 url = url,
1502 is_confidential = is_confidential,
1503 pk_channel_type = pk_channel_type,
1504 pk_identity = self.pk_obj
1505 )
1506 return comm_channel
1507 #--------------------------------------------------------
1509 gmDemographicRecord.delete_comm_channel (
1510 pk = comm_channel['pk_lnk_identity2comm'],
1511 pk_patient = self.pk_obj
1512 )
1513 #--------------------------------------------------------
1514 # contacts API
1515 #--------------------------------------------------------
1517
1518 cmd = "SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
1519 args = {'pat': self.pk_obj}
1520 if address_type is not None:
1521 cmd = cmd + " AND address_type = %(typ)s"
1522 args['typ'] = address_type
1523
1524 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1525
1526 return [
1527 gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'})
1528 for r in rows
1529 ]
1530 #--------------------------------------------------------
1531 - def link_address(self, number=None, street=None, postcode=None, urb=None, region_code=None, country_code=None, subunit=None, suburb=None, id_type=None, address=None, adr_type=None):
1532 """Link an address with a patient, creating the address if it does not exists.
1533
1534 @param id_type The primary key of the address type.
1535 """
1536 if address is None:
1537 address = gmDemographicRecord.create_address (
1538 country_code = country_code,
1539 region_code = region_code,
1540 urb = urb,
1541 suburb = suburb,
1542 postcode = postcode,
1543 street = street,
1544 number = number,
1545 subunit = subunit
1546 )
1547
1548 if address is None:
1549 return None
1550
1551 # already linked ?
1552 cmd = "SELECT id_address FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
1553 args = {'pat': self.pk_obj, 'adr': address['pk_address']}
1554 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1555
1556 # no, link to person
1557 if len(rows) == 0:
1558 args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
1559 cmd = """
1560 INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
1561 VALUES (%(id)s, %(adr)s)
1562 RETURNING id_address"""
1563 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
1564
1565 linked_adr = gmDemographicRecord.cPatientAddress(aPK_obj = rows[0]['id_address'])
1566
1567 # possibly change type
1568 if id_type is None:
1569 if adr_type is not None:
1570 id_type = gmDemographicRecord.create_address_type(address_type = adr_type)
1571 if id_type is not None:
1572 linked_adr['pk_address_type'] = id_type
1573 linked_adr.save()
1574
1575 return linked_adr
1576 #----------------------------------------------------------------------
1578 """Remove an address from the patient.
1579
1580 The address itself stays in the database.
1581 The address can be either cAdress or cPatientAdress.
1582 """
1583 if pk_address is None:
1584 args = {'person': self.pk_obj, 'adr': address['pk_address']}
1585 else:
1586 args = {'person': self.pk_obj, 'adr': pk_address}
1587 cmd = """
1588 DELETE FROM dem.lnk_person_org_address
1589 WHERE
1590 dem.lnk_person_org_address.id_identity = %(person)s
1591 AND
1592 dem.lnk_person_org_address.id_address = %(adr)s
1593 AND
1594 NOT EXISTS(SELECT 1 FROM bill.bill WHERE fk_receiver_address = dem.lnk_person_org_address.id)
1595 """
1596 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1597 #----------------------------------------------------------------------
1598 # bills API
1599 #----------------------------------------------------------------------
1605
1606 bills = property(get_bills, lambda x:x)
1607 #----------------------------------------------------------------------
1608 # relatives API
1609 #----------------------------------------------------------------------
1611 cmd = """
1612 SELECT
1613 d_rt.description,
1614 d_vap.*
1615 FROM
1616 dem.v_all_persons d_vap,
1617 dem.relation_types d_rt,
1618 dem.lnk_person2relative d_lp2r
1619 WHERE
1620 ( d_lp2r.id_identity = %(pk)s
1621 AND
1622 d_vap.pk_identity = d_lp2r.id_relative
1623 AND
1624 d_rt.id = d_lp2r.id_relation_type
1625 ) or (
1626 d_lp2r.id_relative = %(pk)s
1627 AND
1628 d_vap.pk_identity = d_lp2r.id_identity
1629 AND
1630 d_rt.inverse = d_lp2r.id_relation_type
1631 )"""
1632 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
1633 if len(rows) == 0:
1634 return []
1635 return [(row[0], cPerson(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk_identity'})) for row in rows]
1636 #--------------------------------------------------------
1638 # create new relative
1639 id_new_relative = create_dummy_identity()
1640
1641 relative = cPerson(aPK_obj=id_new_relative)
1642 # pre-fill with data from ourselves
1643 # relative.copy_addresses(self)
1644 relative.add_name( '**?**', self.get_names()['lastnames'])
1645 # and link the two
1646 if 'relatives' in self._ext_cache:
1647 del self._ext_cache['relatives']
1648 cmd = """
1649 insert into dem.lnk_person2relative (
1650 id_identity, id_relative, id_relation_type
1651 ) values (
1652 %s, %s, (select id from dem.relation_types where description = %s)
1653 )"""
1654 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type ]}])
1655 return True
1656 #----------------------------------------------------------------------
1660 #--------------------------------------------------------
1662 if self._payload[self._idx['pk_emergency_contact']] is None:
1663 return None
1664 return cPerson(self._payload[self._idx['pk_emergency_contact']])
1665
1666 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1667
1668 #----------------------------------------------------------------------
1669 # age/dob related
1670 #----------------------------------------------------------------------
1672 return gmDateTime.format_dob (
1673 self._payload[self._idx['dob']],
1674 format = format,
1675 none_string = none_string,
1676 dob_is_estimated = self._payload[self._idx['dob_is_estimated']] and honor_estimation
1677 )
1678
1679 #----------------------------------------------------------------------
1681 dob = self['dob']
1682
1683 if dob is None:
1684 return '??'
1685
1686 if dob > gmDateTime.pydt_now_here():
1687 return _('invalid age: DOB in the future')
1688
1689 death = self['deceased']
1690
1691 if death is None:
1692 return '%s%s' % (
1693 gmTools.bool2subst (
1694 self._payload[self._idx['dob_is_estimated']],
1695 gmTools.u_almost_equal_to,
1696 ''
1697 ),
1698 gmDateTime.format_apparent_age_medically (
1699 age = gmDateTime.calculate_apparent_age(start = dob)
1700 )
1701 )
1702
1703 if dob > death:
1704 return _('invalid age: DOB after death')
1705
1706 return '%s%s%s' % (
1707 gmTools.u_latin_cross,
1708 gmTools.bool2subst (
1709 self._payload[self._idx['dob_is_estimated']],
1710 gmTools.u_almost_equal_to,
1711 ''
1712 ),
1713 gmDateTime.format_apparent_age_medically (
1714 age = gmDateTime.calculate_apparent_age (
1715 start = dob,
1716 end = self['deceased']
1717 )
1718 )
1719 )
1720
1721 #----------------------------------------------------------------------
1723 if self['dob'] is None:
1724 return False
1725 cmd = 'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1726 rows, idx = gmPG2.run_ro_queries (
1727 queries = [{
1728 'cmd': cmd,
1729 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1730 }]
1731 )
1732 return rows[0][0]
1733
1734 #----------------------------------------------------------------------
1736 if self['dob'] is None:
1737 return None
1738 now = gmDateTime.pydt_now_here()
1739 if now.month < self['dob'].month:
1740 return False
1741 if now.month > self['dob'].month:
1742 return True
1743 # -> DOB is this month
1744 if now.day < self['dob'].day:
1745 return False
1746 if now.day > self['dob'].day:
1747 return True
1748 # -> DOB is today
1749 return False
1750
1751 current_birthday_passed = property(_get_current_birthday_passed)
1752
1753 #----------------------------------------------------------------------
1755 if self['dob'] is None:
1756 return None
1757 now = gmDateTime.pydt_now_here()
1758 return gmDateTime.pydt_replace (
1759 dt = self['dob'],
1760 year = now.year,
1761 strict = False
1762 )
1763
1764 birthday_this_year = property(_get_birthday_this_year)
1765
1766 #----------------------------------------------------------------------
1768 if self['dob'] is None:
1769 return None
1770 now = gmDateTime.pydt_now_here()
1771 return gmDateTime.pydt_replace (
1772 dt = self['dob'],
1773 year = now.year + 1,
1774 strict = False
1775 )
1776
1777 birthday_next_year = property(_get_birthday_next_year)
1778
1779 #----------------------------------------------------------------------
1781 if self['dob'] is None:
1782 return None
1783 now = gmDateTime.pydt_now_here()
1784 return gmDateTime.pydt_replace (
1785 dt = self['dob'],
1786 year = now.year - 1,
1787 strict = False
1788 )
1789
1790 birthday_last_year = property(_get_birthday_last_year, lambda x:x)
1791
1792 #----------------------------------------------------------------------
1793 # practice related
1794 #----------------------------------------------------------------------
1796 cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
1797 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1798 if len(rows) > 0:
1799 return rows[0]
1800 else:
1801 return None
1802 #--------------------------------------------------------
1804 return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']], order_by = order_by)
1805
1806 messages = property(get_messages, lambda x:x)
1807 #--------------------------------------------------------
1809 return gmProviderInbox.get_overdue_messages(pk_patient = self._payload[self._idx['pk_identity']])
1810
1811 overdue_messages = property(_get_overdue_messages, lambda x:x)
1812
1813 #--------------------------------------------------------
1816
1817 #--------------------------------------------------------
1819 return gmAutoHints.get_hints_for_patient (
1820 pk_identity = self._payload[self._idx['pk_identity']],
1821 pk_encounter = pk_encounter
1822 )
1823
1824 dynamic_hints = property(_get_dynamic_hints, lambda x:x)
1825
1826 #--------------------------------------------------------
1829
1830 suppressed_hints = property(_get_suppressed_hints, lambda x:x)
1831
1832 #--------------------------------------------------------
1834 if self._payload[self._idx['pk_primary_provider']] is None:
1835 return None
1836 cmd = "SELECT * FROM dem.v_all_persons WHERE pk_identity = (SELECT pk_identity FROM dem.v_staff WHERE pk_staff = %(pk_staff)s)"
1837 args = {'pk_staff': self._payload[self._idx['pk_primary_provider']]}
1838 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1839 if len(rows) == 0:
1840 return None
1841 return cPerson(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_identity'})
1842
1843 primary_provider_identity = property(_get_primary_provider_identity, lambda x:x)
1844
1845 #--------------------------------------------------------
1847 if self._payload[self._idx['pk_primary_provider']] is None:
1848 return None
1849 from Gnumed.business import gmStaff
1850 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1851
1852 primary_provider = property(_get_primary_provider, lambda x:x)
1853
1854 #----------------------------------------------------------------------
1855 # convenience
1856 #----------------------------------------------------------------------
1858 """Format patient demographics into patient specific path name fragment."""
1859
1860 return gmTools.fname_sanitize('%s-%s-%s' % (
1861 self._payload[self._idx['lastnames']],
1862 self._payload[self._idx['firstnames']],
1863 self.get_formatted_dob(format = '%Y-%m-%d')
1864 ))
1865 # return (u'%s-%s-%s' % (
1866 # self._payload[self._idx['lastnames']].replace(u' ', u'_'),
1867 # self._payload[self._idx['firstnames']].replace(u' ', u'_'),
1868 # self.get_formatted_dob(format = '%Y-%m-%d')
1869 # )).replace (
1870 # u"'", u""
1871 # ).replace (
1872 # u'"', u''
1873 # ).replace (
1874 # u'/', u'_'
1875 # ).replace (
1876 # u'\\', u'_'
1877 # ).replace (
1878 # u'~', u''
1879 # ).replace (
1880 # u'|', u'_'
1881 # ).replace (
1882 # u'*', u''
1883 # ).replace (
1884 # u'\u2248', u'' # "approximately", having been added by dob_is_estimated
1885 # )
1886
1887
1888 subdir_name = property(get_subdir_name, lambda x:x)
1889
1890 #============================================================
1892 cmd = 'SELECT 1 FROM clin.patient WHERE fk_identity = %(pk_pat)s'
1893 args = {'pk_pat': pk_identity}
1894 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
1895 if len(rows) == 0:
1896 return False
1897 return True
1898
1899 #------------------------------------------------------------
1901 cmd = """
1902 INSERT INTO clin.patient (fk_identity)
1903 SELECT %(pk_ident)s WHERE NOT EXISTS (
1904 SELECT 1 FROM clin.patient c_p WHERE fk_identity = %(pk_ident)s
1905 )"""
1906 args = {'pk_ident': pk_identity}
1907 queries = [{'cmd': cmd, 'args': args}]
1908 gmPG2.run_rw_queries(queries = queries)
1909 return True
1910
1911 #============================================================
1912 # helper functions
1913 #------------------------------------------------------------
1914 _yield = lambda x:x
1915
1917 if not callable(yielder):
1918 raise TypeError('yielder <%s> is not callable' % yielder)
1919 global _yield
1920 _yield = yielder
1921 _log.debug('setting yielder to <%s>', yielder)
1922
1923 #============================================================
1925 """Represents a person which is a patient.
1926
1927 - a specializing subclass of cPerson turning it into a patient
1928 - its use is to cache subobjects like EMR and document folder
1929 """
1931 cPerson.__init__(self, aPK_obj = aPK_obj, row = row)
1932 self.__emr_access_lock = threading.Lock()
1933 self.__emr = None
1934 self.__doc_folder = None
1935
1936 #--------------------------------------------------------
1938 """Do cleanups before dying.
1939
1940 - note that this may be called in a thread
1941 """
1942 if self.__emr is not None:
1943 self.__emr.cleanup()
1944 if self.__doc_folder is not None:
1945 self.__doc_folder.cleanup()
1946 cPerson.cleanup(self)
1947
1948 #----------------------------------------------------------------
1950 from Gnumed.business.gmAllergy import ensure_has_allergy_state
1951 ensure_has_allergy_state(encounter = pk_encounter)
1952 return True
1953
1954 #----------------------------------------------------------
1956 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
1957
1958 # fast path: already set, just return it
1959 if self.__emr is not None:
1960 return self.__emr
1961
1962 stack_logged = False
1963 got_lock = self.__emr_access_lock.acquire(False)
1964 if not got_lock:
1965 # do some logging as we failed to get the lock
1966 call_stack = inspect.stack()
1967 call_stack.reverse()
1968 for idx in range(1, len(call_stack)):
1969 caller = call_stack[idx]
1970 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
1971 del call_stack
1972 stack_logged = True
1973 # now loop a bit
1974 for idx in range(500):
1975 _yield()
1976 time.sleep(0.1)
1977 _yield()
1978 got_lock = self.__emr_access_lock.acquire(False)
1979 if got_lock:
1980 break
1981 if not got_lock:
1982 _log.error('still failed to acquire EMR access lock, aborting (thread [%s])', threading.get_ident())
1983 self.__emr_access_lock.release()
1984 raise AttributeError('cannot lock access to EMR for identity [%s]' % self._payload[self._idx['pk_identity']])
1985
1986 _log.debug('pulling chart for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
1987 if not stack_logged:
1988 # do some logging as we are pulling the chart for the first time
1989 call_stack = inspect.stack()
1990 call_stack.reverse()
1991 for idx in range(1, len(call_stack)):
1992 caller = call_stack[idx]
1993 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
1994 del call_stack
1995 stack_logged = True
1996
1997 self.is_patient = True
1998 from Gnumed.business import gmClinicalRecord
1999 emr = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
2000
2001 _log.debug('returning EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
2002 self.__emr = emr
2003 self.__emr_access_lock.release()
2004 return self.__emr
2005
2006 emr = property(get_emr, lambda x:x)
2007
2008 #----------------------------------------------------------
2010 if self.__doc_folder is None:
2011 self.__doc_folder = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
2012 return self.__doc_folder
2013
2014 document_folder = property(get_document_folder, lambda x:x)
2015
2016 #============================================================
2018 """Patient Borg to hold the currently active patient.
2019
2020 There may be many instances of this but they all share state.
2021
2022 The underlying dem.identity row must have .deleted set to FALSE.
2023
2024 The sequence of events when changing the active patient:
2025
2026 1) Registered callbacks are run.
2027 Those are run synchronously. If a callback
2028 returns False or throws an exception the
2029 patient switch is aborted. Callback code
2030 can rely on the patient still being active
2031 and to not go away until it returns. It
2032 is not passed any arguments and must return
2033 False or True.
2034
2035 2) Signal "pre_patient_unselection" is sent.
2036 This does not wait for nor check results.
2037 The keyword pk_identity contains the
2038 PK of the person being switched away
2039 from.
2040
2041 3) the current patient is unset (gmNull.cNull)
2042
2043 4) Signal "current_patient_unset" is sent
2044 At this point resetting GUI fields to
2045 empty should be done. The active patient
2046 is not there anymore.
2047
2048 This does not wait for nor check results.
2049
2050 5) The current patient is set to the new value.
2051 The new patient can also remain gmNull.cNull
2052 in case the calling code explicitely unset
2053 the current patient.
2054
2055 6) Signal "post_patient_selection" is sent.
2056 Code listening to this signal can
2057 assume that the new patient is
2058 already active.
2059 """
2061 """Change or get currently active patient.
2062
2063 patient:
2064 * None: get currently active patient
2065 * -1: unset currently active patient
2066 * cPatient instance: set active patient if possible
2067 """
2068 # make sure we do have a patient pointer
2069 try:
2070 self.patient
2071 except AttributeError:
2072 self.patient = gmNull.cNull()
2073 self.__register_interests()
2074 # set initial lock state,
2075 # this lock protects against activating another patient
2076 # when we are controlled from a remote application
2077 self.__lock_depth = 0
2078 # initialize callback state
2079 self.__callbacks_before_switching_away_from_patient = []
2080
2081 # user wants copy of current patient
2082 if patient is None:
2083 return None
2084
2085 # do nothing if patient is locked
2086 if self.locked:
2087 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
2088 return None
2089
2090 # user wants to explicitly unset current patient
2091 if patient == -1:
2092 _log.debug('explicitly unsetting current patient')
2093 if not self.__run_callbacks_before_switching_away_from_patient():
2094 _log.error('not unsetting current patient, at least one pre-change callback failed')
2095 return None
2096 self.__send_pre_unselection_notification()
2097 self.patient.cleanup()
2098 self.patient = gmNull.cNull()
2099 self.__send_unselection_notification()
2100 # give it some time
2101 time.sleep(0.5)
2102 self.__send_selection_notification()
2103 return None
2104
2105 # must be cPatient instance, then
2106 if not isinstance(patient, cPatient):
2107 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
2108 raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
2109
2110 # same ID, no change needed
2111 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
2112 return None
2113
2114 if patient['is_deleted']:
2115 _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
2116 raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
2117
2118 # user wants different patient
2119 _log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
2120
2121 if not self.__run_callbacks_before_switching_away_from_patient():
2122 _log.error('not changing current patient, at least one pre-change callback failed')
2123 return None
2124
2125 # everything seems swell
2126 self.__send_pre_unselection_notification()
2127 self.patient.cleanup()
2128 self.patient = gmNull.cNull()
2129 self.__send_unselection_notification()
2130 # give it some time
2131 time.sleep(0.5)
2132 self.patient = patient
2133 # for good measure ...
2134 # however, actually we want to get rid of that
2135 self.patient.emr
2136 self.__send_selection_notification()
2137
2138 return None
2139
2140 #--------------------------------------------------------
2143
2144 #--------------------------------------------------------
2146 # we don't have a patient: don't process signals
2147 if isinstance(self.patient, gmNull.cNull):
2148 return True
2149
2150 # we only care about identity and name changes
2151 if kwds['table'] not in ['dem.identity', 'dem.names']:
2152 return True
2153
2154 # signal is not about our patient: ignore signal
2155 if int(kwds['pk_identity']) != self.patient.ID:
2156 return True
2157
2158 if kwds['table'] == 'dem.identity':
2159 # we don't care about newly INSERTed or DELETEd patients
2160 if kwds['operation'] != 'UPDATE':
2161 return True
2162
2163 self.patient.refetch_payload()
2164 return True
2165
2166 #--------------------------------------------------------
2167 # external API
2168 #--------------------------------------------------------
2170 # callbacks are run synchronously before
2171 # switching *away* from the current patient,
2172 # if a callback returns false the current
2173 # patient will not be switched away from,
2174 # callbacks will not be passed any arguments
2175 if not callable(callback):
2176 raise TypeError('callback [%s] not callable' % callback)
2177
2178 self.__callbacks_before_switching_away_from_patient.append(callback)
2179
2180 #--------------------------------------------------------
2183
2184 connected = property(_get_connected, lambda x:x)
2185
2186 #--------------------------------------------------------
2189
2191 if locked:
2192 self.__lock_depth = self.__lock_depth + 1
2193 gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__)
2194 else:
2195 if self.__lock_depth == 0:
2196 _log.error('lock/unlock imbalance, tried to refcount lock depth below 0')
2197 return
2198 else:
2199 self.__lock_depth = self.__lock_depth - 1
2200 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2201
2202 locked = property(_get_locked, _set_locked)
2203
2204 #--------------------------------------------------------
2206 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
2207 self.__lock_depth = 0
2208 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2209
2210 #--------------------------------------------------------
2211 # patient change handling
2212 #--------------------------------------------------------
2214 if isinstance(self.patient, gmNull.cNull):
2215 return True
2216
2217 for call_back in self.__callbacks_before_switching_away_from_patient:
2218 try:
2219 successful = call_back()
2220 except:
2221 _log.exception('callback [%s] failed', call_back)
2222 print("*** pre-change callback failed ***")
2223 print(type(call_back))
2224 print(call_back)
2225 return False
2226
2227 if not successful:
2228 _log.error('callback [%s] returned False', call_back)
2229 return False
2230
2231 return True
2232
2233 #--------------------------------------------------------
2235 """Sends signal when current patient is about to be unset.
2236
2237 This does NOT wait for signal handlers to complete.
2238 """
2239 kwargs = {
2240 'signal': 'pre_patient_unselection',
2241 'sender': self.__class__.__name__,
2242 'pk_identity': self.patient['pk_identity']
2243 }
2244 gmDispatcher.send(**kwargs)
2245
2246 #--------------------------------------------------------
2248 """Sends signal when the previously active patient has
2249 been unset during a change of active patient.
2250
2251 This is the time to initialize GUI fields to empty values.
2252
2253 This does NOT wait for signal handlers to complete.
2254 """
2255 kwargs = {
2256 'signal': 'current_patient_unset',
2257 'sender': self.__class__.__name__
2258 }
2259 gmDispatcher.send(**kwargs)
2260
2261 #--------------------------------------------------------
2263 """Sends signal when another patient has actually been made active."""
2264 kwargs = {
2265 'signal': 'post_patient_selection',
2266 'sender': self.__class__.__name__,
2267 'pk_identity': self.patient['pk_identity']
2268 }
2269 gmDispatcher.send(**kwargs)
2270
2271 #--------------------------------------------------------
2272 # __getattr__ handling
2273 #--------------------------------------------------------
2275 # override __getattr__ here, not __getattribute__ because
2276 # the former is used _after_ ordinary attribute lookup
2277 # failed while the latter is applied _before_ ordinary
2278 # lookup (and is easy to drive into infinite recursion),
2279 # this is also why subsequent access to self.patient
2280 # simply returns the .patient member value :-)
2281 if attribute == 'patient':
2282 raise AttributeError
2283 if isinstance(self.patient, gmNull.cNull):
2284 _log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute)
2285 raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute))
2286 return getattr(self.patient, attribute)
2287
2288 #--------------------------------------------------------
2289 # __get/setitem__ handling
2290 #--------------------------------------------------------
2292 """Return any attribute if known how to retrieve it by proxy.
2293 """
2294 return self.patient[attribute]
2295
2296 #--------------------------------------------------------
2299
2300 #============================================================
2301 # match providers
2302 #============================================================
2305 gmMatchProvider.cMatchProvider_SQL2.__init__(
2306 self,
2307 queries = [
2308 """SELECT
2309 pk_staff AS data,
2310 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
2311 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
2312 FROM dem.v_staff
2313 WHERE
2314 is_active AND (
2315 short_alias %(fragment_condition)s OR
2316 firstnames %(fragment_condition)s OR
2317 lastnames %(fragment_condition)s OR
2318 db_user %(fragment_condition)s
2319 )
2320 """
2321 ]
2322 )
2323 self.setThresholds(1, 2, 3)
2324
2325 #============================================================
2326 # convenience functions
2327 #============================================================
2329 queries = [{
2330 'cmd': "select dem.add_name(%s, %s, %s, %s)",
2331 'args': [pk_person, firstnames, lastnames, active]
2332 }]
2333 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
2334 name = cPersonName(aPK_obj = rows[0][0])
2335 return name
2336
2337 #============================================================
2339
2340 cmd1 = "INSERT INTO dem.identity (gender, dob, comment) VALUES (%s, %s, %s)"
2341 cmd2 = """
2342 INSERT INTO dem.names (
2343 id_identity, lastnames, firstnames
2344 ) VALUES (
2345 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
2346 ) RETURNING id_identity"""
2347 # cmd2 = u"select dem.add_name(currval('dem.identity_pk_seq')::integer, coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx'), True)"
2348 try:
2349 rows, idx = gmPG2.run_rw_queries (
2350 queries = [
2351 {'cmd': cmd1, 'args': [gender, dob, comment]},
2352 {'cmd': cmd2, 'args': [lastnames, firstnames]}
2353 #{'cmd': cmd2, 'args': [firstnames, lastnames]}
2354 ],
2355 return_data = True
2356 )
2357 except Exception:
2358 _log.exception('cannot create identity')
2359 gmLog2.log_stack_trace()
2360 return None
2361 ident = cPerson(aPK_obj = rows[0][0])
2362 gmHooks.run_hook_script(hook = 'post_person_creation')
2363 return ident
2364
2365 #============================================================
2367 _log.info('disabling identity [%s]', pk_identity)
2368 cmd = "UPDATE dem.identity SET deleted = true WHERE pk = %(pk)s"
2369 args = {'pk': pk_identity}
2370 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2371 return True
2372
2373 #============================================================
2375 cmd = "INSERT INTO dem.identity(gender) VALUES (NULL::text) RETURNING pk"
2376 rows, idx = gmPG2.run_rw_queries (
2377 queries = [{'cmd': cmd}],
2378 return_data = True
2379 )
2380 return gmDemographicRecord.cPerson(aPK_obj = rows[0][0])
2381
2382 #============================================================
2384 cmd = 'SELECT EXISTS(SELECT 1 FROM dem.identity where pk = %(pk)s)'
2385 args = {'pk': pk_identity}
2386 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
2387 return rows[0][0]
2388
2389 #============================================================
2391 """Set active patient.
2392
2393 If patient is -1 the active patient will be UNset.
2394 """
2395 if isinstance(patient, gmCurrentPatient):
2396 return True
2397
2398 if isinstance(patient, cPatient):
2399 pat = patient
2400 elif isinstance(patient, cPerson):
2401 pat = pat.as_patient
2402 elif patient == -1:
2403 pat = patient
2404 else:
2405 # maybe integer ?
2406 success, pk = gmTools.input2int(initial = patient, minval = 1)
2407 if not success:
2408 raise ValueError('<patient> must be either -1, >0, or a cPatient, cPerson or gmCurrentPatient instance, is: %s' % patient)
2409 # but also valid patient ID ?
2410 try:
2411 pat = cPatient(aPK_obj = pk)
2412 except:
2413 _log.exception('identity [%s] not found' % patient)
2414 return False
2415
2416 # attempt to switch
2417 try:
2418 gmCurrentPatient(patient = pat, forced_reload = forced_reload)
2419 except:
2420 _log.exception('error changing active patient to [%s]' % patient)
2421 return False
2422
2423 return True
2424
2425 #============================================================
2426 # gender related
2427 #------------------------------------------------------------
2429 """Retrieves the list of known genders from the database."""
2430 global __gender_idx
2431 global __gender_list
2432
2433 if __gender_list is None:
2434 cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC"
2435 __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
2436 _log.debug('genders in database: %s' % __gender_list)
2437
2438 return (__gender_list, __gender_idx)
2439
2440 #------------------------------------------------------------
2441 map_gender2mf = {
2442 'm': 'm',
2443 'f': 'f',
2444 'tf': 'f',
2445 'tm': 'm',
2446 'h': 'mf'
2447 }
2448
2449 # https://tools.ietf.org/html/rfc6350#section-6.2.7
2450 # M F O N U
2451 map_gender2vcard = {
2452 'm': 'M',
2453 'f': 'F',
2454 'tf': 'F',
2455 'tm': 'M',
2456 'h': 'O',
2457 None: 'U'
2458 }
2459
2460 #------------------------------------------------------------
2461 # maps GNUmed related i18n-aware gender specifiers to a unicode symbol
2462 map_gender2symbol = {
2463 'm': '\u2642',
2464 'f': '\u2640',
2465 'tf': '\u26A5\u2640',
2466 # 'tf': u'\u2642\u2640-\u2640',
2467 'tm': '\u26A5\u2642',
2468 # 'tm': u'\u2642\u2640-\u2642',
2469 'h': '\u26A5',
2470 # 'h': u'\u2642\u2640',
2471 None: '?\u26A5?'
2472 }
2473 #------------------------------------------------------------
2475 """Maps GNUmed related i18n-aware gender specifiers to a human-readable string."""
2476
2477 global __gender2string_map
2478
2479 if __gender2string_map is None:
2480 genders, idx = get_gender_list()
2481 __gender2string_map = {
2482 'm': _('male'),
2483 'f': _('female'),
2484 'tf': '',
2485 'tm': '',
2486 'h': '',
2487 None: _('unknown gender')
2488 }
2489 for g in genders:
2490 __gender2string_map[g[idx['l10n_tag']]] = g[idx['l10n_label']]
2491 __gender2string_map[g[idx['tag']]] = g[idx['l10n_label']]
2492 _log.debug('gender -> string mapping: %s' % __gender2string_map)
2493
2494 return __gender2string_map[gender]
2495 #------------------------------------------------------------
2497 """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
2498
2499 global __gender2salutation_map
2500
2501 if __gender2salutation_map is None:
2502 genders, idx = get_gender_list()
2503 __gender2salutation_map = {
2504 'm': _('Mr'),
2505 'f': _('Mrs'),
2506 'tf': '',
2507 'tm': '',
2508 'h': '',
2509 None: ''
2510 }
2511 for g in genders:
2512 __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]]
2513 __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]]
2514 __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]]
2515 _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map)
2516
2517 return __gender2salutation_map[gender]
2518 #------------------------------------------------------------
2520 """Try getting the gender for the given first name."""
2521
2522 if firstnames is None:
2523 return None
2524
2525 rows, idx = gmPG2.run_ro_queries(queries = [{
2526 'cmd': "SELECT gender FROM dem.name_gender_map WHERE name ILIKE %(fn)s LIMIT 1",
2527 'args': {'fn': firstnames}
2528 }])
2529
2530 if len(rows) == 0:
2531 return None
2532
2533 return rows[0][0]
2534 #============================================================
2536 cmd = 'SELECT pk FROM dem.identity'
2537 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
2538 return [ r[0] for r in rows ]
2539
2540 #============================================================
2542 return [ cPerson(aPK_obj = pk) for pk in pks ]
2543 #============================================================
2545 from Gnumed.business import gmXdtObjects
2546 return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
2547 #============================================================
2549 from Gnumed.business import gmPracSoftAU
2550 return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
2551
2552 #============================================================
2553 # main/testing
2554 #============================================================
2555 if __name__ == '__main__':
2556
2557 if len(sys.argv) == 1:
2558 sys.exit()
2559
2560 if sys.argv[1] != 'test':
2561 sys.exit()
2562
2563 import datetime
2564
2565 gmI18N.activate_locale()
2566 gmI18N.install_domain()
2567 gmDateTime.init()
2568
2569 #--------------------------------------------------------
2571
2572 ident = cPerson(1)
2573 print("setting active patient with", ident)
2574 set_active_patient(patient=ident)
2575
2576 patient = cPatient(12)
2577 print("setting active patient with", patient)
2578 set_active_patient(patient=patient)
2579
2580 pat = gmCurrentPatient()
2581 print(pat['dob'])
2582 #pat['dob'] = 'test'
2583
2584 # staff = cStaff()
2585 # print "setting active patient with", staff
2586 # set_active_patient(patient=staff)
2587
2588 print("setting active patient with -1")
2589 set_active_patient(patient=-1)
2590 #--------------------------------------------------------
2592 dto = cDTO_person()
2593 dto.firstnames = 'Sepp'
2594 dto.lastnames = 'Herberger'
2595 dto.gender = 'male'
2596 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
2597 print(dto)
2598
2599 print(dto['firstnames'])
2600 print(dto['lastnames'])
2601 print(dto['gender'])
2602 print(dto['dob'])
2603
2604 for key in dto.keys():
2605 print(key)
2606 #--------------------------------------------------------
2608 # create patient
2609 print('\n\nCreating identity...')
2610 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
2611 print('Identity created: %s' % new_identity)
2612
2613 print('\nSetting title and gender...')
2614 new_identity['title'] = 'test title';
2615 new_identity['gender'] = 'f';
2616 new_identity.save_payload()
2617 print('Refetching identity from db: %s' % cPerson(aPK_obj=new_identity['pk_identity']))
2618
2619 print('\nGetting all names...')
2620 for a_name in new_identity.get_names():
2621 print(a_name)
2622 print('Active name: %s' % (new_identity.get_active_name()))
2623 print('Setting nickname...')
2624 new_identity.set_nickname(nickname='test nickname')
2625 print('Refetching all names...')
2626 for a_name in new_identity.get_names():
2627 print(a_name)
2628 print('Active name: %s' % (new_identity.get_active_name()))
2629
2630 print('\nIdentity occupations: %s' % new_identity['occupations'])
2631 print('Creating identity occupation...')
2632 new_identity.link_occupation('test occupation')
2633 print('Identity occupations: %s' % new_identity['occupations'])
2634
2635 print('\nIdentity addresses: %s' % new_identity.get_addresses())
2636 print('Creating identity address...')
2637 # make sure the state exists in the backend
2638 new_identity.link_address (
2639 number = 'test 1234',
2640 street = 'test street',
2641 postcode = 'test postcode',
2642 urb = 'test urb',
2643 region_code = 'SN',
2644 country_code = 'DE'
2645 )
2646 print('Identity addresses: %s' % new_identity.get_addresses())
2647
2648 print('\nIdentity communications: %s' % new_identity.get_comm_channels())
2649 print('Creating identity communication...')
2650 new_identity.link_comm_channel('homephone', '1234566')
2651 print('Identity communications: %s' % new_identity.get_comm_channels())
2652 #--------------------------------------------------------
2654 for pk in range(1,16):
2655 name = cPersonName(aPK_obj=pk)
2656 print(name.description)
2657 print(' ', name)
2658 #--------------------------------------------------------
2660 genders, idx = get_gender_list()
2661 print("\n\nRetrieving gender enum (tag, label, weight):")
2662 for gender in genders:
2663 print("%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']]))
2664 #--------------------------------------------------------
2666 person = cPerson(aPK_obj = 12)
2667 print(person)
2668 print(person.export_area)
2669 print(person.export_area.items)
2670 #--------------------------------------------------------
2672 person = cPerson(aPK_obj = 9)
2673 print(person.get_external_ids(id_type='Fachgebiet', issuer='Ärztekammer'))
2674 #print person.get_external_ids()
2675 #--------------------------------------------------------
2679
2680 #--------------------------------------------------------
2684
2685 #--------------------------------------------------------
2689
2690 #--------------------------------------------------------
2692 patient = cPatient(12)
2693 set_active_patient(patient = patient)
2694 curr_pat = gmCurrentPatient()
2695 other_pat = cIdentity(1111111)
2696 curr_pat.assimilate_identity(other_identity=None)
2697
2698 #--------------------------------------------------------
2699 #test_dto_person()
2700 #test_identity()
2701 #test_set_active_pat()
2702 #test_search_by_dto()
2703 #test_name()
2704 #test_gender_list()
2705
2706 #map_gender2salutation('m')
2707 # module functions
2708
2709 #comms = get_comm_list()
2710 #print "\n\nRetrieving communication media enum (id, description): %s" % comms
2711 #test_export_area()
2712 #test_ext_id()
2713 #test_vcf()
2714 #test_ext_id()
2715 #test_current_patient()
2716 test_assimilate_identity()
2717
2718 #============================================================
2719
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sun Aug 19 01:55:20 2018 | http://epydoc.sourceforge.net |