1
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 __version__ = "$Revision: 1.198 $"
9 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
10 __license__ = "GPL"
11
12
13 import sys, os.path, time, re as regex, string, types, datetime as pyDT, codecs, threading, logging
14
15
16
17 if __name__ == '__main__':
18 sys.path.insert(0, '../../')
19 from Gnumed.pycommon import gmExceptions, gmDispatcher, gmBorg, gmI18N, gmNull, gmBusinessDBObject, gmTools
20 from Gnumed.pycommon import gmPG2
21 from Gnumed.pycommon import gmDateTime
22 from Gnumed.pycommon import gmMatchProvider
23 from Gnumed.pycommon import gmLog2
24 from Gnumed.pycommon import gmHooks
25
26 from Gnumed.business import gmDemographicRecord
27 from Gnumed.business import gmClinicalRecord
28 from Gnumed.business import gmXdtMappings
29 from Gnumed.business import gmProviderInbox
30 from Gnumed.business.gmDocuments import cDocumentFolder
31
32
33 _log = logging.getLogger('gm.person')
34 _log.info(__version__)
35
36 __gender_list = None
37 __gender_idx = None
38
39 __gender2salutation_map = None
40
41
42
44
46 self.identity = None
47 self.external_ids = []
48 self.comm_channels = []
49 self.addresses = []
50
51
52
54 return 'firstnames lastnames dob gender'.split()
55
58
60 """Generate generic queries.
61
62 - not locale dependant
63 - data -> firstnames, lastnames, dob, gender
64
65 shall we mogrify name parts ? probably not as external
66 sources should know what they do
67
68 finds by inactive name, too, but then shows
69 the corresponding active name ;-)
70
71 Returns list of matching identities (may be empty)
72 or None if it was told to create an identity but couldn't.
73 """
74 where_snippets = []
75 args = {}
76
77 where_snippets.append(u'firstnames = %(first)s')
78 args['first'] = self.firstnames
79
80 where_snippets.append(u'lastnames = %(last)s')
81 args['last'] = self.lastnames
82
83 if self.dob is not None:
84 where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
85 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
86
87 if self.gender is not None:
88 where_snippets.append('gender = %(sex)s')
89 args['sex'] = self.gender
90
91 cmd = u"""
92 SELECT *, '%s' AS match_type
93 FROM dem.v_basic_person
94 WHERE
95 pk_identity IN (
96 SELECT pk_identity FROM dem.v_person_names WHERE %s
97 )
98 ORDER BY lastnames, firstnames, dob""" % (
99 _('external patient source (name, gender, date of birth)'),
100 ' AND '.join(where_snippets)
101 )
102
103 try:
104 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
105 except:
106 _log.error(u'cannot get candidate identities for dto "%s"' % self)
107 _log.exception('query %s' % cmd)
108 rows = []
109
110 if len(rows) == 0:
111 _log.debug('no candidate identity matches found')
112 if not can_create:
113 return []
114 ident = self.import_into_database()
115 if ident is None:
116 return None
117 identities = [ident]
118 else:
119 identities = [ cIdentity(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
120
121 return identities
122
124 """Imports self into the database."""
125
126 self.identity = create_identity (
127 firstnames = self.firstnames,
128 lastnames = self.lastnames,
129 gender = self.gender,
130 dob = self.dob
131 )
132
133 if self.identity is None:
134 return None
135
136 for ext_id in self.external_ids:
137 try:
138 self.identity.add_external_id (
139 type_name = ext_id['name'],
140 value = ext_id['value'],
141 issuer = ext_id['issuer'],
142 comment = ext_id['comment']
143 )
144 except StandardError:
145 _log.exception('cannot import <external ID> from external data source')
146 _log.log_stack_trace()
147
148 for comm in self.comm_channels:
149 try:
150 self.identity.link_comm_channel (
151 comm_medium = comm['channel'],
152 url = comm['url']
153 )
154 except StandardError:
155 _log.exception('cannot import <comm channel> from external data source')
156 _log.log_stack_trace()
157
158 for adr in self.addresses:
159 try:
160 self.identity.link_address (
161 number = adr['number'],
162 street = adr['street'],
163 postcode = adr['zip'],
164 urb = adr['urb'],
165 state = adr['region'],
166 country = adr['country']
167 )
168 except StandardError:
169 _log.exception('cannot import <address> from external data source')
170 _log.log_stack_trace()
171
172 return self.identity
173
176
178 value = value.strip()
179 if value == u'':
180 return
181 name = name.strip()
182 if name == u'':
183 raise ValueError(_('<name> cannot be empty'))
184 issuer = issuer.strip()
185 if issuer == u'':
186 raise ValueError(_('<issuer> cannot be empty'))
187 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
188
190 url = url.strip()
191 if url == u'':
192 return
193 channel = channel.strip()
194 if channel == u'':
195 raise ValueError(_('<channel> cannot be empty'))
196 self.comm_channels.append({'channel': channel, 'url': url})
197
198 - def remember_address(self, number=None, street=None, urb=None, region=None, zip=None, country=None):
199 number = number.strip()
200 if number == u'':
201 raise ValueError(_('<number> cannot be empty'))
202 street = street.strip()
203 if street == u'':
204 raise ValueError(_('<street> cannot be empty'))
205 urb = urb.strip()
206 if urb == u'':
207 raise ValueError(_('<urb> cannot be empty'))
208 zip = zip.strip()
209 if zip == u'':
210 raise ValueError(_('<zip> cannot be empty'))
211 country = country.strip()
212 if country == u'':
213 raise ValueError(_('<country> cannot be empty'))
214 region = region.strip()
215 if region == u'':
216 region = u'??'
217 self.addresses.append ({
218 u'number': number,
219 u'street': street,
220 u'zip': zip,
221 u'urb': urb,
222 u'region': region,
223 u'country': country
224 })
225
226
227
229 return u'<%s @ %s: %s %s (%s) %s>' % (
230 self.__class__.__name__,
231 id(self),
232 self.firstnames,
233 self.lastnames,
234 self.gender,
235 self.dob
236 )
237
239 """Do some sanity checks on self.* access."""
240
241 if attr == 'gender':
242 glist, idx = get_gender_list()
243 for gender in glist:
244 if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
245 val = gender[idx['tag']]
246 object.__setattr__(self, attr, val)
247 return
248 raise ValueError('invalid gender: [%s]' % val)
249
250 if attr == 'dob':
251 if val is not None:
252 if not isinstance(val, pyDT.datetime):
253 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
254 if val.tzinfo is None:
255 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
256
257 object.__setattr__(self, attr, val)
258 return
259
261 return getattr(self, attr)
262
263 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
264 _cmd_fetch_payload = u"SELECT * FROM dem.v_person_names WHERE pk_name = %s"
265 _cmds_store_payload = [
266 u"""UPDATE dem.names SET
267 active = FALSE
268 WHERE
269 %(active_name)s IS TRUE -- act only when needed and only
270 AND
271 id_identity = %(pk_identity)s -- on names of this identity
272 AND
273 active IS TRUE -- which are active
274 AND
275 id != %(pk_name)s -- but NOT *this* name
276 """,
277 u"""update dem.names set
278 active = %(active_name)s,
279 preferred = %(preferred)s,
280 comment = %(comment)s
281 where
282 id = %(pk_name)s and
283 id_identity = %(pk_identity)s and -- belt and suspenders
284 xmin = %(xmin_name)s""",
285 u"""select xmin as xmin_name from dem.names where id = %(pk_name)s"""
286 ]
287 _updatable_fields = ['active_name', 'preferred', 'comment']
288
297
299 return '%(last)s, %(title)s %(first)s%(nick)s' % {
300 'last': self._payload[self._idx['lastnames']],
301 'title': gmTools.coalesce (
302 self._payload[self._idx['title']],
303 map_gender2salutation(self._payload[self._idx['gender']])
304 ),
305 'first': self._payload[self._idx['firstnames']],
306 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u' "%s"', u'%s')
307 }
308
309 description = property(_get_description, lambda x:x)
310
311 -class cIdentity(gmBusinessDBObject.cBusinessDBObject):
312 _cmd_fetch_payload = u"SELECT * FROM dem.v_basic_person WHERE pk_identity = %s"
313 _cmds_store_payload = [
314 u"""UPDATE dem.identity SET
315 gender = %(gender)s,
316 dob = %(dob)s,
317 tob = %(tob)s,
318 cob = gm.nullify_empty_string(%(cob)s),
319 title = gm.nullify_empty_string(%(title)s),
320 fk_marital_status = %(pk_marital_status)s,
321 karyotype = gm.nullify_empty_string(%(karyotype)s),
322 pupic = gm.nullify_empty_string(%(pupic)s),
323 deceased = %(deceased)s,
324 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
325 fk_emergency_contact = %(pk_emergency_contact)s,
326 fk_primary_provider = %(pk_primary_provider)s,
327 comment = gm.nullify_empty_string(%(comment)s)
328 WHERE
329 pk = %(pk_identity)s and
330 xmin = %(xmin_identity)s
331 RETURNING
332 xmin AS xmin_identity"""
333 ]
334 _updatable_fields = [
335 "title",
336 "dob",
337 "tob",
338 "cob",
339 "gender",
340 "pk_marital_status",
341 "karyotype",
342 "pupic",
343 'deceased',
344 'emergency_contact',
345 'pk_emergency_contact',
346 'pk_primary_provider',
347 'comment'
348 ]
349
351 return self._payload[self._idx['pk_identity']]
353 raise AttributeError('setting ID of identity is not allowed')
354 ID = property(_get_ID, _set_ID)
355
357
358 if attribute == 'dob':
359 if value is not None:
360
361 if isinstance(value, pyDT.datetime):
362 if value.tzinfo is None:
363 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
364 else:
365 raise TypeError, '[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value)
366
367
368 if self._payload[self._idx['dob']] is not None:
369 old_dob = gmDateTime.pydt_strftime (
370 self._payload[self._idx['dob']],
371 format = '%Y %m %d %H %M %S',
372 accuracy = gmDateTime.acc_seconds
373 )
374 new_dob = gmDateTime.pydt_strftime (
375 value,
376 format = '%Y %m %d %H %M %S',
377 accuracy = gmDateTime.acc_seconds
378 )
379 if new_dob == old_dob:
380 return
381
382 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
383
386
388 cmd = u"""
389 SELECT EXISTS (
390 SELECT 1
391 FROM clin.v_emr_journal
392 WHERE
393 pk_patient = %(pat)s
394 AND
395 soap_cat IS NOT NULL
396 )"""
397 args = {'pat': self._payload[self._idx['pk_identity']]}
398 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
399 return rows[0][0]
400
402 raise AttributeError('setting is_patient status of identity is not allowed')
403
404 is_patient = property(_get_is_patient, _set_is_patient)
405
407 cmd = u"SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
408 args = {'pk': self._payload[self._idx['pk_identity']]}
409 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
410 if len(rows) == 0:
411 return None
412 return rows[0][0]
413
414 staff_id = property(_get_staff_id, lambda x:x)
415
416
417
420
421 gender_symbol = property(_get_gender_symbol, lambda x:x)
422
424 for name in self.get_names():
425 if name['active_name'] is True:
426 return name
427
428 _log.error('cannot retrieve active name for patient [%s]' % self._payload[self._idx['pk_identity']])
429 return None
430
432 cmd = u"""
433 SELECT *
434 FROM dem.v_person_names
435 WHERE pk_identity = %(pk_pat)s
436 ORDER BY active_name DESC, lastnames, firstnames
437 """
438 args = {'pk_pat': self._payload[self._idx['pk_identity']]}
439 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
440
441 if len(rows) == 0:
442
443 return []
444
445 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
446 return names
447
455
457 return _(u'%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)') % {
458 'last': self._payload[self._idx['lastnames']],
459 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
460 'first': self._payload[self._idx['firstnames']],
461 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'"),
462 'sex': self.gender_symbol
463 }
464
466 return _(u'%(last)s,%(title)s %(first)s%(nick)s') % {
467 'last': self._payload[self._idx['lastnames']],
468 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
469 'first': self._payload[self._idx['firstnames']],
470 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'")
471 }
472
473 - def add_name(self, firstnames, lastnames, active=True):
474 """Add a name.
475
476 @param firstnames The first names.
477 @param lastnames The last names.
478 @param active When True, the new name will become the active one (hence setting other names to inactive)
479 @type active A types.BooleanType instance
480 """
481 name = create_name(self.ID, firstnames, lastnames, active)
482 if active:
483 self.refetch_payload()
484 return name
485
487 cmd = u"delete from dem.names where id = %(name)s and id_identity = %(pat)s"
488 args = {'name': name['pk_name'], 'pat': self.ID}
489 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
490
491
492
493
495 """
496 Set the nickname. Setting the nickname only makes sense for the currently
497 active name.
498 @param nickname The preferred/nick/warrior name to set.
499 """
500 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"select dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
501 self.refetch_payload()
502 return True
503
514
516 args = {
517 u'tag': tag,
518 u'identity': self.ID
519 }
520
521
522 cmd = u"SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
523 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
524 if len(rows) > 0:
525 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
526
527
528 cmd = u"""
529 INSERT INTO dem.identity_tag (
530 fk_tag,
531 fk_identity
532 ) VALUES (
533 %(tag)s,
534 %(identity)s
535 )
536 RETURNING pk
537 """
538 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
539 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
540
542 cmd = u"DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
543 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
544
545
546
547
548
549
550
551
552
553
554 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
555 """Adds an external ID to the patient.
556
557 creates ID type if necessary
558 """
559
560
561 if pk_type is not None:
562 cmd = u"""
563 select * from dem.v_external_ids4identity where
564 pk_identity = %(pat)s and
565 pk_type = %(pk_type)s and
566 value = %(val)s"""
567 else:
568
569 if issuer is None:
570 cmd = u"""
571 select * from dem.v_external_ids4identity where
572 pk_identity = %(pat)s and
573 name = %(name)s and
574 value = %(val)s"""
575 else:
576 cmd = u"""
577 select * from dem.v_external_ids4identity where
578 pk_identity = %(pat)s and
579 name = %(name)s and
580 value = %(val)s and
581 issuer = %(issuer)s"""
582 args = {
583 'pat': self.ID,
584 'name': type_name,
585 'val': value,
586 'issuer': issuer,
587 'pk_type': pk_type
588 }
589 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
590
591
592 if len(rows) == 0:
593
594 args = {
595 'pat': self.ID,
596 'val': value,
597 'type_name': type_name,
598 'pk_type': pk_type,
599 'issuer': issuer,
600 'comment': comment
601 }
602
603 if pk_type is None:
604 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
605 %(val)s,
606 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
607 %(comment)s,
608 %(pat)s
609 )"""
610 else:
611 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
612 %(val)s,
613 %(pk_type)s,
614 %(comment)s,
615 %(pat)s
616 )"""
617
618 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
619
620
621 else:
622 row = rows[0]
623 if comment is not None:
624
625 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
626 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
627 cmd = u"update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
628 args = {'comment': comment, 'pk': row['pk_id']}
629 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
630
631 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
632 """Edits an existing external ID.
633
634 Creates ID type if necessary.
635 """
636 cmd = u"""
637 UPDATE dem.lnk_identity2ext_id SET
638 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
639 external_id = %(value)s,
640 comment = gm.nullify_empty_string(%(comment)s)
641 WHERE
642 id = %(pk)s
643 """
644 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
645 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
646
648 where_parts = ['pk_identity = %(pat)s']
649 args = {'pat': self.ID}
650
651 if id_type is not None:
652 where_parts.append(u'name = %(name)s')
653 args['name'] = id_type.strip()
654
655 if issuer is not None:
656 where_parts.append(u'issuer = %(issuer)s')
657 args['issuer'] = issuer.strip()
658
659 cmd = u"SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
660 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
661
662 return rows
663
665 cmd = u"""
666 delete from dem.lnk_identity2ext_id
667 where id_identity = %(pat)s and id = %(pk)s"""
668 args = {'pat': self.ID, 'pk': pk_ext_id}
669 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
670
672 """Merge another identity into this one.
673
674 Keep this one. Delete other one."""
675
676 if other_identity.ID == self.ID:
677 return True, None
678
679 curr_pat = gmCurrentPatient()
680 if curr_pat.connected:
681 if other_identity.ID == curr_pat.ID:
682 return False, _('Cannot merge active patient into another patient.')
683
684 queries = []
685 args = {'old_pat': other_identity.ID, 'new_pat': self.ID}
686
687
688 queries.append ({
689 'cmd': u'delete from clin.allergy_state where pk = (select pk_allergy_state from clin.v_pat_allergy_state where pk_patient = %(old_pat)s)',
690 'args': args
691 })
692
693
694
695 queries.append ({
696 'cmd': u'update dem.names set active = False where id_identity = %(old_pat)s',
697 'args': args
698 })
699
700
701 FKs = gmPG2.get_foreign_keys2column (
702 schema = u'dem',
703 table = u'identity',
704 column = u'pk'
705 )
706
707
708 cmd_template = u'update %s set %s = %%(new_pat)s where %s = %%(old_pat)s'
709 for FK in FKs:
710 queries.append ({
711 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
712 'args': args
713 })
714
715
716 queries.append ({
717 'cmd': u'delete from dem.identity where pk = %(old_pat)s',
718 'args': args
719 })
720
721 _log.warning('identity [%s] is about to assimilate identity [%s]', self.ID, other_identity.ID)
722
723 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
724
725 self.add_external_id (
726 type_name = u'merged GNUmed identity primary key',
727 value = u'GNUmed::pk::%s' % other_identity.ID,
728 issuer = u'GNUmed'
729 )
730
731 return True, None
732
733
735 cmd = u"""
736 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
737 values (
738 %(pat)s,
739 %(urg)s,
740 %(cmt)s,
741 %(area)s,
742 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
743 )"""
744 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
745 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose=True)
746
747 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
748
749 template = u'%s%s%s\r\n'
750
751 file = codecs.open (
752 filename = filename,
753 mode = 'wb',
754 encoding = encoding,
755 errors = 'strict'
756 )
757
758 file.write(template % (u'013', u'8000', u'6301'))
759 file.write(template % (u'013', u'9218', u'2.10'))
760 if external_id_type is None:
761 file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID))
762 else:
763 ext_ids = self.get_external_ids(id_type = external_id_type)
764 if len(ext_ids) > 0:
765 file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value']))
766 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']]))
767 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']]))
768 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), u'3103', self._payload[self._idx['dob']].strftime('%d%m%Y')))
769 file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
770 file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding'))
771 file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding))
772 if external_id_type is None:
773 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
774 file.write(template % (u'017', u'6333', u'internal'))
775 else:
776 if len(ext_ids) > 0:
777 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
778 file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type))
779
780 file.close()
781
782
783
786
788 """Link an occupation with a patient, creating the occupation if it does not exists.
789
790 @param occupation The name of the occupation to link the patient to.
791 """
792 if (activities is None) and (occupation is None):
793 return True
794
795 occupation = occupation.strip()
796 if len(occupation) == 0:
797 return True
798
799 if activities is not None:
800 activities = activities.strip()
801
802 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
803
804 cmd = u"select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
805 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
806
807 queries = []
808 if len(rows) == 0:
809 queries.append ({
810 'cmd': u"INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
811 'args': args
812 })
813 else:
814 if rows[0]['activities'] != activities:
815 queries.append ({
816 'cmd': u"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))",
817 'args': args
818 })
819
820 rows, idx = gmPG2.run_rw_queries(queries = queries)
821
822 return True
823
825 if occupation is None:
826 return True
827 occupation = occupation.strip()
828 cmd = u"delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
829 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
830 return True
831
832
833
835 cmd = u"select * from dem.v_person_comms where pk_identity = %s"
836 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
837
838 filtered = rows
839
840 if comm_medium is not None:
841 filtered = []
842 for row in rows:
843 if row['comm_type'] == comm_medium:
844 filtered.append(row)
845
846 return [ gmDemographicRecord.cCommChannel(row = {
847 'pk_field': 'pk_lnk_identity2comm',
848 'data': r,
849 'idx': idx
850 }) for r in filtered
851 ]
852
853 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
854 """Link a communication medium with a patient.
855
856 @param comm_medium The name of the communication medium.
857 @param url The communication resource locator.
858 @type url A types.StringType instance.
859 @param is_confidential Wether the data must be treated as confidential.
860 @type is_confidential A types.BooleanType instance.
861 """
862 comm_channel = gmDemographicRecord.create_comm_channel (
863 comm_medium = comm_medium,
864 url = url,
865 is_confidential = is_confidential,
866 pk_channel_type = pk_channel_type,
867 pk_identity = self.pk_obj
868 )
869 return comm_channel
870
876
877
878
880 cmd = u"select * from dem.v_pat_addresses where pk_identity=%s"
881 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx=True)
882 addresses = []
883 for r in rows:
884 addresses.append(gmDemographicRecord.cPatientAddress(row={'idx': idx, 'data': r, 'pk_field': 'pk_address'}))
885
886 filtered = addresses
887
888 if address_type is not None:
889 filtered = []
890 for adr in addresses:
891 if adr['address_type'] == address_type:
892 filtered.append(adr)
893
894 return filtered
895
896 - def link_address(self, number=None, street=None, postcode=None, urb=None, state=None, country=None, subunit=None, suburb=None, id_type=None, address=None):
897 """Link an address with a patient, creating the address if it does not exists.
898
899 @param number The number of the address.
900 @param street The name of the street.
901 @param postcode The postal code of the address.
902 @param urb The name of town/city/etc.
903 @param state The code of the state.
904 @param country The code of the country.
905 @param id_type The primary key of the address type.
906 """
907 if address is None:
908
909 address = gmDemographicRecord.create_address (
910 country = country,
911 state = state,
912 urb = urb,
913 suburb = suburb,
914 postcode = postcode,
915 street = street,
916 number = number,
917 subunit = subunit
918 )
919
920 if address is None:
921 return None
922
923
924 cmd = u"SELECT * FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
925 args = {'pat': self.pk_obj, 'adr': address['pk_address']}
926 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
927
928
929 if len(rows) == 0:
930 args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
931 cmd = u"""
932 INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
933 VALUES (%(id)s, %(adr)s)
934 RETURNING *"""
935 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
936
937
938 if id_type is not None:
939 r = rows[0]
940 if r['id_type'] != id_type:
941 cmd = "UPDATE dem.lnk_person_org_address SET id_type = %(type)s WHERE id = %(id)s"
942 args = {'type': id_type, 'id': r['id']}
943 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
944
945 return address
946
948 """Remove an address from the patient.
949
950 The address itself stays in the database.
951 The address can be either cAdress or cPatientAdress.
952 """
953 if pk_address is None:
954 args = {'person': self.pk_obj, 'adr': address['pk_address']}
955 else:
956 args = {'person': self.pk_obj, 'adr': pk_address}
957 cmd = u"DELETE FROM dem.lnk_person_org_address WHERE id_identity = %(person)s AND id_address = %(adr)s"
958 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
959
960
961
963 cmd = u"""
964 select
965 t.description,
966 vbp.pk_identity as id,
967 title,
968 firstnames,
969 lastnames,
970 dob,
971 cob,
972 gender,
973 karyotype,
974 pupic,
975 pk_marital_status,
976 marital_status,
977 xmin_identity,
978 preferred
979 from
980 dem.v_basic_person vbp, dem.relation_types t, dem.lnk_person2relative l
981 where
982 (
983 l.id_identity = %(pk)s and
984 vbp.pk_identity = l.id_relative and
985 t.id = l.id_relation_type
986 ) or (
987 l.id_relative = %(pk)s and
988 vbp.pk_identity = l.id_identity and
989 t.inverse = l.id_relation_type
990 )"""
991 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
992 if len(rows) == 0:
993 return []
994 return [(row[0], cIdentity(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk'})) for row in rows]
995
997
998 id_new_relative = create_dummy_identity()
999
1000 relative = cIdentity(aPK_obj=id_new_relative)
1001
1002
1003 relative.add_name( '**?**', self.get_names()['lastnames'])
1004
1005 if self._ext_cache.has_key('relatives'):
1006 del self._ext_cache['relatives']
1007 cmd = u"""
1008 insert into dem.lnk_person2relative (
1009 id_identity, id_relative, id_relation_type
1010 ) values (
1011 %s, %s, (select id from dem.relation_types where description = %s)
1012 )"""
1013 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type ]}])
1014 return True
1015
1017
1018 self.set_relative(None, relation)
1019
1024
1025 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1026
1027
1028
1057
1058 - def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
1059 cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1060 rows, idx = gmPG2.run_ro_queries (
1061 queries = [{
1062 'cmd': cmd,
1063 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1064 }]
1065 )
1066 return rows[0][0]
1067
1068
1069
1071 cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s'
1072 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1073 if len(rows) > 0:
1074 return rows[0]
1075 else:
1076 return None
1077
1080
1083
1084 messages = property(_get_messages, _set_messages)
1085
1088
1090 if self._payload[self._idx['pk_primary_provider']] is None:
1091 return None
1092 from Gnumed.business import gmStaff
1093 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1094
1095 primary_provider = property(_get_primary_provider, lambda x:x)
1096
1097
1098
1100 """Format patient demographics into patient specific path name fragment."""
1101 return '%s-%s%s-%s' % (
1102 self._payload[self._idx['lastnames']].replace(u' ', u'_'),
1103 self._payload[self._idx['firstnames']].replace(u' ', u'_'),
1104 gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)'),
1105 self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding())
1106 )
1107
1109 """Represents a person which is a patient.
1110
1111 - a specializing subclass of cIdentity turning it into a patient
1112 - its use is to cache subobjects like EMR and document folder
1113 """
1114 - def __init__(self, aPK_obj=None, row=None):
1115 cIdentity.__init__(self, aPK_obj=aPK_obj, row=row)
1116 self.__db_cache = {}
1117 self.__emr_access_lock = threading.Lock()
1118
1120 """Do cleanups before dying.
1121
1122 - note that this may be called in a thread
1123 """
1124 if self.__db_cache.has_key('clinical record'):
1125 self.__db_cache['clinical record'].cleanup()
1126 if self.__db_cache.has_key('document folder'):
1127 self.__db_cache['document folder'].cleanup()
1128 cIdentity.cleanup(self)
1129
1131
1132
1133
1134
1135
1136
1137
1138
1139 if not self.__emr_access_lock.acquire(False):
1140 raise AttributeError('cannot access EMR')
1141 try:
1142 emr = self.__db_cache['clinical record']
1143 self.__emr_access_lock.release()
1144 return emr
1145 except KeyError:
1146 pass
1147
1148 self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
1149 self.__emr_access_lock.release()
1150 return self.__db_cache['clinical record']
1151
1153 try:
1154 return self.__db_cache['document folder']
1155 except KeyError:
1156 pass
1157
1158 self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
1159 return self.__db_cache['document folder']
1160
1162 """Patient Borg to hold currently active patient.
1163
1164 There may be many instances of this but they all share state.
1165 """
1166 - def __init__(self, patient=None, forced_reload=False):
1167 """Change or get currently active patient.
1168
1169 patient:
1170 * None: get currently active patient
1171 * -1: unset currently active patient
1172 * cPatient instance: set active patient if possible
1173 """
1174
1175 try:
1176 tmp = self.patient
1177 except AttributeError:
1178 self.patient = gmNull.cNull()
1179 self.__register_interests()
1180
1181
1182
1183 self.__lock_depth = 0
1184
1185 self.__pre_selection_callbacks = []
1186
1187
1188 if patient is None:
1189 return None
1190
1191
1192 if self.locked:
1193 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
1194 return None
1195
1196
1197 if patient == -1:
1198 _log.debug('explicitly unsetting current patient')
1199 if not self.__run_pre_selection_callbacks():
1200 _log.debug('not unsetting current patient')
1201 return None
1202 self.__send_pre_selection_notification()
1203 self.patient.cleanup()
1204 self.patient = gmNull.cNull()
1205 self.__send_selection_notification()
1206 return None
1207
1208
1209 if not isinstance(patient, cPatient):
1210 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
1211 raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)
1212
1213
1214 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
1215 return None
1216
1217
1218 _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
1219
1220
1221 if not self.__run_pre_selection_callbacks():
1222 _log.debug('not changing current patient')
1223 return None
1224 self.__send_pre_selection_notification()
1225 self.patient.cleanup()
1226 self.patient = patient
1227 self.patient.get_emr()
1228 self.__send_selection_notification()
1229
1230 return None
1231
1235
1239
1240
1241
1243 if not callable(callback):
1244 raise TypeError(u'callback [%s] not callable' % callback)
1245
1246 self.__pre_selection_callbacks.append(callback)
1247
1250
1252 raise AttributeError(u'invalid to set <connected> state')
1253
1254 connected = property(_get_connected, _set_connected)
1255
1257 return (self.__lock_depth > 0)
1258
1260 if locked:
1261 self.__lock_depth = self.__lock_depth + 1
1262 gmDispatcher.send(signal='patient_locked')
1263 else:
1264 if self.__lock_depth == 0:
1265 _log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
1266 return
1267 else:
1268 self.__lock_depth = self.__lock_depth - 1
1269 gmDispatcher.send(signal='patient_unlocked')
1270
1271 locked = property(_get_locked, _set_locked)
1272
1274 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
1275 self.__lock_depth = 0
1276 gmDispatcher.send(signal='patient_unlocked')
1277
1278
1279
1281 if isinstance(self.patient, gmNull.cNull):
1282 return True
1283
1284 for call_back in self.__pre_selection_callbacks:
1285 try:
1286 successful = call_back()
1287 except:
1288 _log.exception('callback [%s] failed', call_back)
1289 print "*** pre-selection callback failed ***"
1290 print type(call_back)
1291 print call_back
1292 return False
1293
1294 if not successful:
1295 _log.debug('callback [%s] returned False', call_back)
1296 return False
1297
1298 return True
1299
1301 """Sends signal when another patient is about to become active.
1302
1303 This does NOT wait for signal handlers to complete.
1304 """
1305 kwargs = {
1306 'signal': u'pre_patient_selection',
1307 'sender': id(self.__class__),
1308 'pk_identity': self.patient['pk_identity']
1309 }
1310 gmDispatcher.send(**kwargs)
1311
1313 """Sends signal when another patient has actually been made active."""
1314 kwargs = {
1315 'signal': u'post_patient_selection',
1316 'sender': id(self.__class__),
1317 'pk_identity': self.patient['pk_identity']
1318 }
1319 gmDispatcher.send(**kwargs)
1320
1321
1322
1324 if attribute == 'patient':
1325 raise AttributeError
1326 if not isinstance(self.patient, gmNull.cNull):
1327 return getattr(self.patient, attribute)
1328
1329
1330
1332 """Return any attribute if known how to retrieve it by proxy.
1333 """
1334 return self.patient[attribute]
1335
1338
1339
1340
1343 gmMatchProvider.cMatchProvider_SQL2.__init__(
1344 self,
1345 queries = [
1346 u"""SELECT
1347 pk_staff AS data,
1348 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
1349 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
1350 FROM dem.v_staff
1351 WHERE
1352 is_active AND (
1353 short_alias %(fragment_condition)s OR
1354 firstnames %(fragment_condition)s OR
1355 lastnames %(fragment_condition)s OR
1356 db_user %(fragment_condition)s
1357 )
1358 """
1359 ]
1360 )
1361 self.setThresholds(1, 2, 3)
1362
1363
1364
1365 -def create_name(pk_person, firstnames, lastnames, active=False):
1366 queries = [{
1367 'cmd': u"select dem.add_name(%s, %s, %s, %s)",
1368 'args': [pk_person, firstnames, lastnames, active]
1369 }]
1370 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
1371 name = cPersonName(aPK_obj = rows[0][0])
1372 return name
1373
1374 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
1375
1376 cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)"""
1377 cmd2 = u"""
1378 INSERT INTO dem.names (
1379 id_identity, lastnames, firstnames
1380 ) VALUES (
1381 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
1382 ) RETURNING id_identity"""
1383 rows, idx = gmPG2.run_rw_queries (
1384 queries = [
1385 {'cmd': cmd1, 'args': [gender, dob]},
1386 {'cmd': cmd2, 'args': [lastnames, firstnames]}
1387 ],
1388 return_data = True
1389 )
1390 ident = cIdentity(aPK_obj=rows[0][0])
1391 gmHooks.run_hook_script(hook = u'post_person_creation')
1392 return ident
1393
1401
1428
1429
1430
1441
1442 map_gender2mf = {
1443 'm': u'm',
1444 'f': u'f',
1445 'tf': u'f',
1446 'tm': u'm',
1447 'h': u'mf'
1448 }
1449
1450
1451 map_gender2symbol = {
1452 'm': u'\u2642',
1453 'f': u'\u2640',
1454 'tf': u'\u26A5\u2640',
1455 'tm': u'\u26A5\u2642',
1456 'h': u'\u26A5'
1457
1458
1459
1460 }
1461
1482
1484 """Try getting the gender for the given first name."""
1485
1486 if firstnames is None:
1487 return None
1488
1489 rows, idx = gmPG2.run_ro_queries(queries = [{
1490 'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1",
1491 'args': {'fn': firstnames}
1492 }])
1493
1494 if len(rows) == 0:
1495 return None
1496
1497 return rows[0][0]
1498
1500 return [ cIdentity(aPK_obj = pk) for pk in pks ]
1501
1505
1509
1510
1511
1512 if __name__ == '__main__':
1513
1514 if len(sys.argv) == 1:
1515 sys.exit()
1516
1517 if sys.argv[1] != 'test':
1518 sys.exit()
1519
1520 import datetime
1521
1522 gmI18N.activate_locale()
1523 gmI18N.install_domain()
1524 gmDateTime.init()
1525
1526
1547
1549 dto = cDTO_person()
1550 dto.firstnames = 'Sepp'
1551 dto.lastnames = 'Herberger'
1552 dto.gender = 'male'
1553 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
1554 print dto
1555
1556 print dto['firstnames']
1557 print dto['lastnames']
1558 print dto['gender']
1559 print dto['dob']
1560
1561 for key in dto.keys():
1562 print key
1563
1565
1566 print '\n\nCreating identity...'
1567 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
1568 print 'Identity created: %s' % new_identity
1569
1570 print '\nSetting title and gender...'
1571 new_identity['title'] = 'test title';
1572 new_identity['gender'] = 'f';
1573 new_identity.save_payload()
1574 print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity'])
1575
1576 print '\nGetting all names...'
1577 for a_name in new_identity.get_names():
1578 print a_name
1579 print 'Active name: %s' % (new_identity.get_active_name())
1580 print 'Setting nickname...'
1581 new_identity.set_nickname(nickname='test nickname')
1582 print 'Refetching all names...'
1583 for a_name in new_identity.get_names():
1584 print a_name
1585 print 'Active name: %s' % (new_identity.get_active_name())
1586
1587 print '\nIdentity occupations: %s' % new_identity['occupations']
1588 print 'Creating identity occupation...'
1589 new_identity.link_occupation('test occupation')
1590 print 'Identity occupations: %s' % new_identity['occupations']
1591
1592 print '\nIdentity addresses: %s' % new_identity.get_addresses()
1593 print 'Creating identity address...'
1594
1595 new_identity.link_address (
1596 number = 'test 1234',
1597 street = 'test street',
1598 postcode = 'test postcode',
1599 urb = 'test urb',
1600 state = 'SN',
1601 country = 'DE'
1602 )
1603 print 'Identity addresses: %s' % new_identity.get_addresses()
1604
1605 print '\nIdentity communications: %s' % new_identity.get_comm_channels()
1606 print 'Creating identity communication...'
1607 new_identity.link_comm_channel('homephone', '1234566')
1608 print 'Identity communications: %s' % new_identity.get_comm_channels()
1609
1615
1616
1617
1618
1619
1620 test_name()
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633