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 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
9 __license__ = "GPL"
10
11
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
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
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
110
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
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
150
152 return 'firstnames lastnames dob gender title'.split()
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
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
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
419 return getattr(self, attr)
420
421
422 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
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
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
474 -class cPerson(gmBusinessDBObject.cBusinessDBObject):
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
510 return self._payload[self._idx['pk_identity']]
512 raise AttributeError('setting ID of identity is not allowed')
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
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
552
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
579
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
605 - def get_names(self, active_only=False, exclude_active=False):
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
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
655 - def add_name(self, firstnames, lastnames, active=True):
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
674
675
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
688
689
690
691 self._payload[self._idx['preferred']] = nickname
692
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
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
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
743
744
745
746
747
748
749
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
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
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
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
817 else:
818 row = rows[0]
819 if comment is not None:
820
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
828 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
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
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
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
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
991
992
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
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
1021
1022
1023
1024
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
1044
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
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
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
1095 FKs = gmPG2.get_foreign_keys2column (
1096 schema = 'dem',
1097 table = 'identity',
1098 column = 'pk'
1099 )
1100
1101 FKs.extend (gmPG2.get_foreign_keys2column (
1102 schema = 'clin',
1103 table = 'patient',
1104 column = 'fk_identity'
1105 ))
1106
1107
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
1118 queries.append ({
1119 'cmd': 'DELETE FROM clin.patient WHERE fk_identity = %(pat2del)s',
1120 'args': args
1121 })
1122
1123
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
1186 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
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
1242
1243
1244
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
1320
1321
1322
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
1337
1338
1339 vc.add('title')
1340 vc.title.value = gmTools.coalesce(self._payload[self._idx['title']], '')
1341 vc.add('gender')
1342
1343 vc.gender.value = map_gender2vcard[self._payload[self._idx['gender']]]
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
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
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
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
1513
1514
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
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
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
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
1599
1600 - def get_bills(self, order_by=None, pk_patient=None):
1601 return gmBilling.get_bills (
1602 order_by = order_by,
1603 pk_patient = self.pk_obj
1604 )
1605
1606 bills = property(get_bills, lambda x:x)
1607
1608
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
1639 id_new_relative = create_dummy_identity()
1640
1641 relative = cPerson(aPK_obj=id_new_relative)
1642
1643
1644 relative.add_name( '**?**', self.get_names()['lastnames'])
1645
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
1658
1659 self.set_relative(None, relation)
1660
1665
1666 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1667
1668
1669
1670
1678
1679
1720
1721
1722 - def dob_in_range(self, min_distance='1 week', max_distance='1 week'):
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
1744 if now.day < self['dob'].day:
1745 return False
1746 if now.day > self['dob'].day:
1747 return True
1748
1749 return False
1750
1751 current_birthday_passed = property(_get_current_birthday_passed)
1752
1753
1763
1764 birthday_this_year = property(_get_birthday_this_year)
1765
1766
1776
1777 birthday_next_year = property(_get_birthday_next_year)
1778
1779
1789
1790 birthday_last_year = property(_get_birthday_last_year, lambda x:x)
1791
1792
1793
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
1805
1806 messages = property(get_messages, lambda x:x)
1807
1810
1811 overdue_messages = property(_get_overdue_messages, lambda x:x)
1812
1813
1816
1817
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
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
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
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
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 """
1930 - def __init__(self, aPK_obj=None, row=None):
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
1953
1954
1956 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
1957
1958
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
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
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
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 """
2060 - def __init__(self, patient=None, forced_reload=False):
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
2069 try:
2070 self.patient
2071 except AttributeError:
2072 self.patient = gmNull.cNull()
2073 self.__register_interests()
2074
2075
2076
2077 self.__lock_depth = 0
2078
2079 self.__callbacks_before_switching_away_from_patient = []
2080
2081
2082 if patient is None:
2083 return None
2084
2085
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
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
2101 time.sleep(0.5)
2102 self.__send_selection_notification()
2103 return None
2104
2105
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
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
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
2126 self.__send_pre_unselection_notification()
2127 self.patient.cleanup()
2128 self.patient = gmNull.cNull()
2129 self.__send_unselection_notification()
2130
2131 time.sleep(0.5)
2132 self.patient = patient
2133
2134
2135 self.patient.emr
2136 self.__send_selection_notification()
2137
2138 return None
2139
2140
2143
2144
2146
2147 if isinstance(self.patient, gmNull.cNull):
2148 return True
2149
2150
2151 if kwds['table'] not in ['dem.identity', 'dem.names']:
2152 return True
2153
2154
2155 if int(kwds['pk_identity']) != self.patient.ID:
2156 return True
2157
2158 if kwds['table'] == 'dem.identity':
2159
2160 if kwds['operation'] != 'UPDATE':
2161 return True
2162
2163 self.patient.refetch_payload()
2164 return True
2165
2166
2167
2168
2170
2171
2172
2173
2174
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
2188 return (self.__lock_depth > 0)
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
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
2273
2275
2276
2277
2278
2279
2280
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
2290
2292 """Return any attribute if known how to retrieve it by proxy.
2293 """
2294 return self.patient[attribute]
2295
2296
2299
2300
2301
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
2327
2328 -def create_name(pk_person, firstnames, lastnames, active=False):
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
2338 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None, comment=None):
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
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
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
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
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
2410 try:
2411 pat = cPatient(aPK_obj = pk)
2412 except:
2413 _log.exception('identity [%s] not found' % patient)
2414 return False
2415
2416
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
2427
2439
2440
2441 map_gender2mf = {
2442 'm': 'm',
2443 'f': 'f',
2444 'tf': 'f',
2445 'tm': 'm',
2446 'h': 'mf'
2447 }
2448
2449
2450
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
2462 map_gender2symbol = {
2463 'm': '\u2642',
2464 'f': '\u2640',
2465 'tf': '\u26A5\u2640',
2466
2467 'tm': '\u26A5\u2642',
2468
2469 'h': '\u26A5',
2470
2471 None: '?\u26A5?'
2472 }
2473
2495
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
2547
2551
2552
2553
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
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
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
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
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
2670
2674
2675
2679
2680
2684
2685
2689
2690
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716 test_assimilate_identity()
2717
2718
2719