1
2
3 from __future__ import print_function
4
5 """GNUmed patient objects.
6
7 This is a patient object intended to let a useful client-side
8 API crystallize from actual use in true XP fashion.
9 """
10
11 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
12 __license__ = "GPL"
13
14
15 import sys
16 import os.path
17 import time
18 import re as regex
19 import datetime as pyDT
20 import threading
21 import logging
22 import io
23 import inspect
24 from xml.etree import ElementTree as etree
25
26
27
28 if __name__ == '__main__':
29 logging.basicConfig(level = logging.DEBUG)
30 sys.path.insert(0, '../../')
31 from Gnumed.pycommon import gmExceptions
32 from Gnumed.pycommon import gmDispatcher
33 from Gnumed.pycommon import gmBorg
34 from Gnumed.pycommon import gmI18N
35 if __name__ == '__main__':
36 gmI18N.activate_locale()
37 gmI18N.install_domain()
38 from Gnumed.pycommon import gmNull
39 from Gnumed.pycommon import gmBusinessDBObject
40 from Gnumed.pycommon import gmTools
41 from Gnumed.pycommon import gmPG2
42 from Gnumed.pycommon import gmDateTime
43 from Gnumed.pycommon import gmMatchProvider
44 from Gnumed.pycommon import gmLog2
45 from Gnumed.pycommon import gmHooks
46
47 from Gnumed.business import gmDemographicRecord
48 from Gnumed.business import gmClinicalRecord
49 from Gnumed.business import gmXdtMappings
50 from Gnumed.business import gmProviderInbox
51 from Gnumed.business import gmExportArea
52 from Gnumed.business import gmBilling
53 from Gnumed.business import gmAutoHints
54 from Gnumed.business.gmDocuments import cDocumentFolder
55
56
57 _log = logging.getLogger('gm.person')
58
59 __gender_list = None
60 __gender_idx = None
61
62 __gender2salutation_map = None
63 __gender2string_map = None
64
65
66 _MERGE_SCRIPT_HEADER = """-- GNUmed patient merge script
67 -- created: %(date)s
68 -- patient to keep : #%(pat2keep)s
69 -- patient to merge: #%(pat2del)s
70 --
71 -- You can EASILY cause mangled data by uncritically applying this script, so ...
72 -- ... BE POSITIVELY SURE YOU UNDERSTAND THE FULL EXTENT OF WHAT IT DOES !
73
74
75 --set default_transaction_read_only to off;
76
77 BEGIN;
78 """
79
80
82 cmd = 'SELECT COUNT(1) FROM dem.lnk_identity2ext_id WHERE fk_origin = %(issuer)s AND external_id = %(val)s'
83 args = {'issuer': pk_issuer, 'val': value}
84 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
85 return rows[0][0]
86
87
89 args = {
90 'last': lastnames,
91 'dob': dob
92 }
93 where_parts = [
94 "lastnames = %(last)s",
95 "dem.date_trunc_utc('day', dob) = dem.date_trunc_utc('day', %(dob)s)"
96 ]
97 if firstnames is not None:
98 if firstnames.strip() != '':
99
100 where_parts.append("firstnames ~* %(first)s")
101 args['first'] = '\\m' + firstnames
102 if active_only:
103 cmd = """SELECT COUNT(1) FROM dem.v_active_persons WHERE %s""" % ' AND '.join(where_parts)
104 else:
105 cmd = """SELECT COUNT(1) FROM dem.v_all_persons WHERE %s""" % ' AND '.join(where_parts)
106 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
107 return rows[0][0]
108
109
111
112
113 if comment is not None:
114 comment = comment.strip()
115 if comment == u'':
116 comment = None
117 args = {
118 'last': lastnames.strip(),
119 'first': firstnames.strip(),
120 'dob': dob,
121 'cmt': comment
122 }
123 where_parts = [
124 u'lower(lastnames) = lower(%(last)s)',
125 u'lower(firstnames) = lower(%(first)s)',
126 u"dem.date_trunc_utc('day', dob) IS NOT DISTINCT FROM dem.date_trunc_utc('day', %(dob)s)",
127 u'lower(comment) IS NOT DISTINCT FROM lower(%(cmt)s)'
128 ]
129 cmd = u"SELECT COUNT(1) FROM dem.v_persons WHERE %s" % u' AND '.join(where_parts)
130 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
131 return rows[0][0]
132
133
134
136
138 self.identity = None
139 self.external_ids = []
140 self.comm_channels = []
141 self.addresses = []
142
143 self.firstnames = None
144 self.lastnames = None
145 self.title = None
146 self.gender = None
147 self.dob = None
148 self.dob_is_estimated = False
149 self.source = self.__class__.__name__
150
151 self.dob_formats = None
152 self.dob_tz = None
153
154
155
156
158 return 'firstnames lastnames dob gender title'.split()
159
162
164 where_snippets = [
165 'firstnames = %(first)s',
166 'lastnames = %(last)s'
167 ]
168 args = {
169 'first': self.firstnames,
170 'last': self.lastnames
171 }
172 if self.dob is not None:
173 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
174 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
175 if self.gender is not None:
176 where_snippets.append('gender = %(sex)s')
177 args['sex'] = self.gender
178 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
179 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
180
181 return rows[0][0] == 1
182
183 is_unique = property(_is_unique)
184
185
187 where_snippets = [
188 'firstnames = %(first)s',
189 'lastnames = %(last)s'
190 ]
191 args = {
192 'first': self.firstnames,
193 'last': self.lastnames
194 }
195 if self.dob is not None:
196 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
197 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
198 if self.gender is not None:
199 where_snippets.append('gender = %(sex)s')
200 args['sex'] = self.gender
201 cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
202 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
203
204 return rows[0][0] > 0
205
206 exists = property(_exists)
207
208
210 """Generate generic queries.
211
212 - not locale dependant
213 - data -> firstnames, lastnames, dob, gender
214
215 shall we mogrify name parts ? probably not as external
216 sources should know what they do
217
218 finds by inactive name, too, but then shows
219 the corresponding active name ;-)
220
221 Returns list of matching identities (may be empty)
222 or None if it was told to create an identity but couldn't.
223 """
224 where_snippets = []
225 args = {}
226
227 where_snippets.append('lower(firstnames) = lower(%(first)s)')
228 args['first'] = self.firstnames
229
230 where_snippets.append('lower(lastnames) = lower(%(last)s)')
231 args['last'] = self.lastnames
232
233 if self.dob is not None:
234 where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
235 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
236
237 if self.gender is not None:
238 where_snippets.append('lower(gender) = lower(%(sex)s)')
239 args['sex'] = self.gender
240
241
242 cmd = """
243 SELECT *, '%s' AS match_type
244 FROM dem.v_active_persons
245 WHERE
246 pk_identity IN (
247 SELECT pk_identity FROM dem.v_person_names WHERE %s
248 )
249 ORDER BY lastnames, firstnames, dob""" % (
250 _('external patient source (name, gender, date of birth)'),
251 ' AND '.join(where_snippets)
252 )
253
254 try:
255 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
256 except Exception:
257 _log.error('cannot get candidate identities for dto "%s"' % self)
258 _log.exception('query %s' % cmd)
259 rows = []
260
261 if len(rows) == 0:
262 _log.debug('no candidate identity matches found')
263 if not can_create:
264 return []
265 ident = self.import_into_database()
266 if ident is None:
267 return None
268 identities = [ident]
269 else:
270 identities = [ cPerson(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
271
272 return identities
273
275 """Imports self into the database."""
276
277 self.identity = create_identity (
278 firstnames = self.firstnames,
279 lastnames = self.lastnames,
280 gender = self.gender,
281 dob = self.dob
282 )
283
284 if self.identity is None:
285 return None
286
287 self.identity['dob_is_estimated'] = self.dob_is_estimated is True
288 if self.title is not None:
289 self.identity['title'] = self.title
290 self.identity.save()
291
292 for ext_id in self.external_ids:
293 try:
294 self.identity.add_external_id (
295 type_name = ext_id['name'],
296 value = ext_id['value'],
297 issuer = ext_id['issuer'],
298 comment = ext_id['comment']
299 )
300 except Exception:
301 _log.exception('cannot import <external ID> from external data source')
302 gmLog2.log_stack_trace()
303
304 for comm in self.comm_channels:
305 try:
306 self.identity.link_comm_channel (
307 comm_medium = comm['channel'],
308 url = comm['url']
309 )
310 except Exception:
311 _log.exception('cannot import <comm channel> from external data source')
312 gmLog2.log_stack_trace()
313
314 for adr in self.addresses:
315 try:
316 self.identity.link_address (
317 adr_type = adr['type'],
318 number = adr['number'],
319 subunit = adr['subunit'],
320 street = adr['street'],
321 postcode = adr['zip'],
322 urb = adr['urb'],
323 region_code = adr['region_code'],
324 country_code = adr['country_code']
325 )
326 except Exception:
327 _log.exception('cannot import <address> from external data source')
328 gmLog2.log_stack_trace()
329
330 return self.identity
331
334
336 value = value.strip()
337 if value == '':
338 return
339 name = name.strip()
340 if name == '':
341 raise ValueError(_('<name> cannot be empty'))
342 issuer = issuer.strip()
343 if issuer == '':
344 raise ValueError(_('<issuer> cannot be empty'))
345 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
346
348 url = url.strip()
349 if url == '':
350 return
351 channel = channel.strip()
352 if channel == '':
353 raise ValueError(_('<channel> cannot be empty'))
354 self.comm_channels.append({'channel': channel, 'url': url})
355
356 - def remember_address(self, number=None, street=None, urb=None, region_code=None, zip=None, country_code=None, adr_type=None, subunit=None):
357 number = number.strip()
358 if number == '':
359 raise ValueError(_('<number> cannot be empty'))
360 street = street.strip()
361 if street == '':
362 raise ValueError(_('<street> cannot be empty'))
363 urb = urb.strip()
364 if urb == '':
365 raise ValueError(_('<urb> cannot be empty'))
366 zip = zip.strip()
367 if zip == '':
368 raise ValueError(_('<zip> cannot be empty'))
369 country_code = country_code.strip()
370 if country_code == '':
371 raise ValueError(_('<country_code> cannot be empty'))
372 if region_code is not None:
373 region_code = region_code.strip()
374 if region_code in [None, '']:
375 region_code = '??'
376 self.addresses.append ({
377 'type': adr_type,
378 'number': number,
379 'subunit': subunit,
380 'street': street,
381 'zip': zip,
382 'urb': urb,
383 'region_code': region_code,
384 'country_code': country_code
385 })
386
387
388
390 return '<%s (%s) @ %s: %s %s (%s) %s%s>' % (
391 self.__class__.__name__,
392 self.source,
393 id(self),
394 self.lastnames.upper(),
395 self.firstnames,
396 self.gender,
397 gmTools.bool2subst(self.dob_is_estimated, '~', '', ''),
398 self.dob
399 )
400
402 """Do some sanity checks on self.* access."""
403
404 if attr == 'gender':
405 if val is None:
406 object.__setattr__(self, attr, val)
407 return
408 glist, idx = get_gender_list()
409 for gender in glist:
410 if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
411 val = gender[idx['tag']]
412 object.__setattr__(self, attr, val)
413 return
414 raise ValueError('invalid gender: [%s]' % val)
415
416 if attr == 'dob':
417 if val is not None:
418 if isinstance(val, str):
419 dob = self.__parse_dob_str(val)
420 if dob is None:
421 raise ValueError('cannot parse DOB [%s]' % val)
422 val = dob
423 if not isinstance(val, pyDT.datetime):
424 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
425 if val.tzinfo is None:
426 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
427
428 object.__setattr__(self, attr, val)
429 return
430
432 return getattr(self, attr)
433
435 if self.dob_formats is None:
436 return None
437 for dob_format in self.dob_formats:
438 try:
439 dob = pyDT.datetime.strptime(dob_str, dob_format)
440 except ValueError:
441 _log.exception('cannot parse DOB [%s] with [%s]', dob_str, dob_format)
442 continue
443 if self.dob_tz is None:
444 raise ValueError('lacking TZ information in DOB [%s] and/or format [%s]' % (dob_str, self.dob_format))
445 dob = dob.replace(tzinfo = self.dob_tz)
446 return dob
447 return None
448
449
450 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
451 _cmd_fetch_payload = "SELECT * FROM dem.v_person_names WHERE pk_name = %s"
452 _cmds_store_payload = [
453 """UPDATE dem.names SET
454 active = FALSE
455 WHERE
456 %(active_name)s IS TRUE -- act only when needed and only
457 AND
458 id_identity = %(pk_identity)s -- on names of this identity
459 AND
460 active IS TRUE -- which are active
461 AND
462 id != %(pk_name)s -- but NOT *this* name
463 """,
464 """update dem.names set
465 active = %(active_name)s,
466 preferred = %(preferred)s,
467 comment = %(comment)s
468 where
469 id = %(pk_name)s and
470 id_identity = %(pk_identity)s and -- belt and suspenders
471 xmin = %(xmin_name)s""",
472 """select xmin as xmin_name from dem.names where id = %(pk_name)s"""
473 ]
474 _updatable_fields = ['active_name', 'preferred', 'comment']
475
484
486 return '%(last)s, %(title)s %(first)s%(nick)s' % {
487 'last': self._payload[self._idx['lastnames']],
488 'title': gmTools.coalesce (
489 self._payload[self._idx['title']],
490 map_gender2salutation(self._payload[self._idx['gender']])
491 ),
492 'first': self._payload[self._idx['firstnames']],
493 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'", '%s')
494 }
495
496 description = property(_get_description, lambda x:x)
497
498
499 _SQL_get_active_person = "SELECT * FROM dem.v_active_persons WHERE pk_identity = %s"
500 _SQL_get_any_person = "SELECT * FROM dem.v_all_persons WHERE pk_identity = %s"
501
502 -class cPerson(gmBusinessDBObject.cBusinessDBObject):
503 _cmd_fetch_payload = _SQL_get_any_person
504 _cmds_store_payload = [
505 """UPDATE dem.identity SET
506 gender = %(gender)s,
507 dob = %(dob)s,
508 dob_is_estimated = %(dob_is_estimated)s,
509 tob = %(tob)s,
510 title = gm.nullify_empty_string(%(title)s),
511 fk_marital_status = %(pk_marital_status)s,
512 deceased = %(deceased)s,
513 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
514 fk_emergency_contact = %(pk_emergency_contact)s,
515 fk_primary_provider = %(pk_primary_provider)s,
516 comment = gm.nullify_empty_string(%(comment)s)
517 WHERE
518 pk = %(pk_identity)s and
519 xmin = %(xmin_identity)s
520 RETURNING
521 xmin AS xmin_identity"""
522 ]
523 _updatable_fields = [
524 "title",
525 "dob",
526 "tob",
527 "gender",
528 "pk_marital_status",
529 'deceased',
530 'emergency_contact',
531 'pk_emergency_contact',
532 'pk_primary_provider',
533 'comment',
534 'dob_is_estimated'
535 ]
536
538 return self._payload[self._idx['pk_identity']]
540 raise AttributeError('setting ID of identity is not allowed')
541
542 ID = property(_get_ID, _set_ID)
543
544
546
547 if attribute == 'dob':
548 if value is not None:
549
550 if isinstance(value, pyDT.datetime):
551 if value.tzinfo is None:
552 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
553 else:
554 raise TypeError('[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value))
555
556
557 if self._payload[self._idx['dob']] is not None:
558 old_dob = gmDateTime.pydt_strftime (
559 self._payload[self._idx['dob']],
560 format = '%Y %m %d %H %M %S',
561 accuracy = gmDateTime.acc_seconds
562 )
563 new_dob = gmDateTime.pydt_strftime (
564 value,
565 format = '%Y %m %d %H %M %S',
566 accuracy = gmDateTime.acc_seconds
567 )
568 if new_dob == old_dob:
569 return
570
571 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
572
573
576
577
580
585
586 is_patient = property(_get_is_patient, _set_is_patient)
587
588
590 return cPatient(self._payload[self._idx['pk_identity']])
591
592 as_patient = property(_get_as_patient, lambda x:x)
593
594
596 cmd = "SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
597 args = {'pk': self._payload[self._idx['pk_identity']]}
598 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
599 if len(rows) == 0:
600 return None
601 return rows[0][0]
602
603 staff_id = property(_get_staff_id, lambda x:x)
604
605
606
607
610
611 gender_symbol = property(_get_gender_symbol, lambda x:x)
612
615
616 gender_string = property(_get_gender_string, lambda x:x)
617
621
622 gender_list = property(_get_gender_list, lambda x:x)
623
625 names = self.get_names(active_only = True)
626 if len(names) == 0:
627 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
628 return None
629 return names[0]
630
631 active_name = property(get_active_name, lambda x:x)
632
633 - def get_names(self, active_only=False, exclude_active=False):
634
635 args = {'pk_pat': self._payload[self._idx['pk_identity']]}
636 where_parts = ['pk_identity = %(pk_pat)s']
637 if active_only:
638 where_parts.append('active_name is True')
639 if exclude_active:
640 where_parts.append('active_name is False')
641 cmd = """
642 SELECT *
643 FROM dem.v_person_names
644 WHERE %s
645 ORDER BY active_name DESC, lastnames, firstnames
646 """ % ' AND '.join(where_parts)
647 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
648
649 if len(rows) == 0:
650
651 return []
652
653 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
654 return names
655
657 if with_nickname:
658 template = _('%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)')
659 else:
660 template = _('%(last)s,%(title)s %(first)s (%(sex)s)')
661 return template % {
662 'last': self._payload[self._idx['lastnames']],
663 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
664 'first': self._payload[self._idx['firstnames']],
665 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'"),
666 'sex': self.gender_symbol
667 }
668
669
671 if with_nickname:
672 template = _('%(last)s,%(title)s %(first)s%(nick)s')
673 else:
674 template = _('%(last)s,%(title)s %(first)s')
675 return template % {
676 'last': self._payload[self._idx['lastnames']],
677 'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
678 'first': self._payload[self._idx['firstnames']],
679 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'")
680 }
681
682
683 - def add_name(self, firstnames, lastnames, active=True):
684 """Add a name.
685
686 @param firstnames The first names.
687 @param lastnames The last names.
688 @param active When True, the new name will become the active one (hence setting other names to inactive)
689 @type active A bool instance
690 """
691 name = create_name(self.ID, firstnames, lastnames, active)
692 if active:
693 self.refetch_payload()
694 return name
695
696
698 cmd = "delete from dem.names where id = %(name)s and id_identity = %(pat)s"
699 args = {'name': name['pk_name'], 'pat': self.ID}
700 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
701
702
703
704
705
707 """
708 Set the nickname. Setting the nickname only makes sense for the currently
709 active name.
710 @param nickname The preferred/nick/warrior name to set.
711 """
712 if self._payload[self._idx['preferred']] == nickname:
713 return True
714 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': "SELECT dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
715
716
717
718
719 self._payload[self._idx['preferred']] = nickname
720
721 return True
722
723
734
735 tags = property(get_tags, lambda x:x)
736
737
739 args = {
740 'tag': tag,
741 'identity': self.ID
742 }
743
744
745 cmd = "SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
746 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
747 if len(rows) > 0:
748 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
749
750
751 cmd = """
752 INSERT INTO dem.identity_tag (
753 fk_tag,
754 fk_identity
755 ) VALUES (
756 %(tag)s,
757 %(identity)s
758 )
759 RETURNING pk
760 """
761 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
762 return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
763
764
766 cmd = "DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
767 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
768
769
770
771
772
773
774
775
776
777
778
779 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
780 """Adds an external ID to the patient.
781
782 creates ID type if necessary
783 """
784
785 if pk_type is not None:
786 cmd = """
787 select * from dem.v_external_ids4identity where
788 pk_identity = %(pat)s and
789 pk_type = %(pk_type)s and
790 value = %(val)s"""
791 else:
792
793 if issuer is None:
794 cmd = """
795 select * from dem.v_external_ids4identity where
796 pk_identity = %(pat)s and
797 name = %(name)s and
798 value = %(val)s"""
799 else:
800 cmd = """
801 select * from dem.v_external_ids4identity where
802 pk_identity = %(pat)s and
803 name = %(name)s and
804 value = %(val)s and
805 issuer = %(issuer)s"""
806 args = {
807 'pat': self.ID,
808 'name': type_name,
809 'val': value,
810 'issuer': issuer,
811 'pk_type': pk_type
812 }
813 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
814
815
816 if len(rows) == 0:
817
818 args = {
819 'pat': self.ID,
820 'val': value,
821 'type_name': type_name,
822 'pk_type': pk_type,
823 'issuer': issuer,
824 'comment': comment
825 }
826
827 if pk_type is None:
828 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
829 %(val)s,
830 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
831 %(comment)s,
832 %(pat)s
833 )"""
834 else:
835 cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
836 %(val)s,
837 %(pk_type)s,
838 %(comment)s,
839 %(pat)s
840 )"""
841
842 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
843
844
845 else:
846 row = rows[0]
847 if comment is not None:
848
849 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
850 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
851 cmd = "update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
852 args = {'comment': comment, 'pk': row['pk_id']}
853 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
854
855
856 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
857 """Edits an existing external ID.
858
859 Creates ID type if necessary.
860 """
861 cmd = """
862 UPDATE dem.lnk_identity2ext_id SET
863 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
864 external_id = %(value)s,
865 comment = gm.nullify_empty_string(%(comment)s)
866 WHERE
867 id = %(pk)s
868 """
869 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
870 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
871
872
874 where_parts = ['pk_identity = %(pat)s']
875 args = {'pat': self.ID}
876
877 if id_type is not None:
878 where_parts.append('name = %(name)s')
879 args['name'] = id_type.strip()
880
881 if issuer is not None:
882 where_parts.append('issuer = %(issuer)s')
883 args['issuer'] = issuer.strip()
884
885 cmd = "SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
886 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
887
888 return rows
889
890 external_ids = property(get_external_ids, lambda x:x)
891
892
894 cmd = """
895 DELETE FROM dem.lnk_identity2ext_id
896 WHERE id_identity = %(pat)s AND id = %(pk)s"""
897 args = {'pat': self.ID, 'pk': pk_ext_id}
898 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
899
900
902 name = self.active_name
903 last = ' '.join(p for p in name['lastnames'].split("-"))
904 last = ' '.join(p for p in last.split("."))
905 last = ' '.join(p for p in last.split("'"))
906 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
907 first = ' '.join(p for p in name['firstnames'].split("-"))
908 first = ' '.join(p for p in first.split("."))
909 first = ' '.join(p for p in first.split("'"))
910 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
911 suggestion = 'GMd-%s%s%s%s%s' % (
912 gmTools.coalesce(target, '', '%s-'),
913 last,
914 first,
915 self.get_formatted_dob(format = '-%Y%m%d', none_string = ''),
916 gmTools.coalesce(self['gender'], '', '-%s')
917 )
918 try:
919 import unidecode
920 return unidecode.unidecode(suggestion)
921 except ImportError:
922 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
923 if encoding is None:
924 return suggestion
925 return suggestion.encode(encoding)
926
927 external_id_suggestion = property(suggest_external_id, lambda x:x)
928
929
931 names2use = [self.active_name]
932 names2use.extend(self.get_names(active_only = False, exclude_active = True))
933 target = gmTools.coalesce(target, '', '%s-')
934 dob = self.get_formatted_dob(format = '-%Y%m%d', none_string = '')
935 gender = gmTools.coalesce(self['gender'], '', '-%s')
936 suggestions = []
937 for name in names2use:
938 last = ' '.join(p for p in name['lastnames'].split("-"))
939 last = ' '.join(p for p in last.split("."))
940 last = ' '.join(p for p in last.split("'"))
941 last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
942 first = ' '.join(p for p in name['firstnames'].split("-"))
943 first = ' '.join(p for p in first.split("."))
944 first = ' '.join(p for p in first.split("'"))
945 first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
946 suggestion = 'GMd-%s%s%s%s%s' % (target, last, first, dob, gender)
947 try:
948 import unidecode
949 suggestions.append(unidecode.unidecode(suggestion))
950 continue
951 except ImportError:
952 _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
953 if encoding is None:
954 suggestions.append(suggestion)
955 else:
956 suggestions.append(suggestion.encode(encoding))
957 return suggestions
958
959
960
962 """Merge another identity into this one.
963
964 Keep this one. Delete other one."""
965
966 if other_identity.ID == self.ID:
967 return True, None
968
969 curr_pat = gmCurrentPatient()
970 if curr_pat.connected:
971 if other_identity.ID == curr_pat.ID:
972 return False, _('Cannot merge active patient into another patient.')
973
974 now_here = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())
975 distinguisher = _('merge of #%s into #%s @ %s') % (other_identity.ID, self.ID, now_here)
976
977 queries = []
978 args = {'pat2del': other_identity.ID, 'pat2keep': self.ID}
979
980
981 queries.append ({
982 'cmd': """
983 UPDATE clin.allergy_state SET
984 has_allergy = greatest (
985 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
986 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
987 ),
988 -- perhaps use least() to play it safe and make it appear longer ago than it might have been, actually ?
989 last_confirmed = greatest (
990 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
991 (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
992 )
993 WHERE
994 pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
995 """,
996 'args': args
997 })
998
999 queries.append ({
1000 'cmd': 'DELETE FROM clin.allergy_state WHERE pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s)',
1001 'args': args
1002 })
1003
1004
1005 queries.append ({
1006 'cmd': """
1007 UPDATE clin.patient SET
1008 edc = coalesce (
1009 edc,
1010 (SELECT edc FROM clin.patient WHERE fk_identity = %(pat2del)s)
1011 )
1012 WHERE
1013 fk_identity = %(pat2keep)s
1014 """,
1015 'args': args
1016 })
1017
1018
1019
1020
1021 queries.append ({
1022 'cmd': """
1023 UPDATE dem.names d_n1 SET
1024 comment = coalesce (
1025 comment, ''
1026 ) || coalesce (
1027 ' (from identity: "' || (SELECT comment FROM dem.identity WHERE pk = %%(pat2del)s) || '")',
1028 ''
1029 ) || ' (during: "%s")'
1030 WHERE
1031 d_n1.id_identity = %%(pat2del)s
1032 """ % distinguisher,
1033 'args': args
1034 })
1035
1036 queries.append ({
1037 'cmd': u"""
1038 UPDATE dem.names d_n SET
1039 id_identity = %(pat2keep)s,
1040 lastnames = lastnames || ' [' || random()::TEXT || ']'
1041 WHERE
1042 d_n.id_identity = %(pat2del)s
1043 AND
1044 d_n.active IS false
1045 """,
1046 'args': args
1047 })
1048
1049
1050
1051
1052
1053 queries.append ({
1054 'cmd': """
1055 INSERT INTO dem.names (
1056 id_identity, active, firstnames, preferred, comment,
1057 lastnames
1058 )
1059 SELECT
1060 %(pat2keep)s, false, firstnames, preferred, comment,
1061 lastnames || ' [' || random()::text || ']'
1062 FROM dem.names d_n
1063 WHERE
1064 d_n.id_identity = %(pat2del)s
1065 AND
1066 d_n.active IS true
1067 """,
1068 'args': args
1069 })
1070
1071
1072
1073 queries.append ({
1074 'cmd': """
1075 UPDATE dem.lnk_identity2comm
1076 SET url = url || ' (%s)'
1077 WHERE
1078 fk_identity = %%(pat2del)s
1079 AND
1080 EXISTS (
1081 SELECT 1 FROM dem.lnk_identity2comm d_li2c
1082 WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url
1083 )
1084 """ % distinguisher,
1085 'args': args
1086 })
1087
1088 queries.append ({
1089 'cmd': """
1090 UPDATE dem.lnk_identity2ext_id
1091 SET external_id = external_id || ' (%s)'
1092 WHERE
1093 id_identity = %%(pat2del)s
1094 AND
1095 EXISTS (
1096 SELECT 1 FROM dem.lnk_identity2ext_id d_li2e
1097 WHERE
1098 d_li2e.id_identity = %%(pat2keep)s
1099 AND
1100 d_li2e.external_id = external_id
1101 AND
1102 d_li2e.fk_origin = fk_origin
1103 )
1104 """ % distinguisher,
1105 'args': args
1106 })
1107
1108 queries.append ({
1109 'cmd': """
1110 DELETE FROM dem.lnk_person_org_address
1111 WHERE
1112 id_identity = %(pat2del)s
1113 AND
1114 id_address IN (
1115 SELECT id_address FROM dem.lnk_person_org_address d_lpoa
1116 WHERE d_lpoa.id_identity = %(pat2keep)s
1117 )
1118 """,
1119 'args': args
1120 })
1121
1122
1123 FKs = gmPG2.get_foreign_keys2column (
1124 schema = 'dem',
1125 table = 'identity',
1126 column = 'pk'
1127 )
1128
1129 FKs.extend (gmPG2.get_foreign_keys2column (
1130 schema = 'clin',
1131 table = 'patient',
1132 column = 'fk_identity'
1133 ))
1134
1135
1136 cmd_template = 'UPDATE %s SET %s = %%(pat2keep)s WHERE %s = %%(pat2del)s'
1137 for FK in FKs:
1138 if FK['referencing_table'] in ['dem.names', 'clin.patient']:
1139 continue
1140 queries.append ({
1141 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
1142 'args': args
1143 })
1144
1145
1146 queries.append ({
1147 'cmd': 'DELETE FROM clin.patient WHERE fk_identity = %(pat2del)s',
1148 'args': args
1149 })
1150
1151
1152 queries.append ({
1153 'cmd': 'delete from dem.identity where pk = %(pat2del)s',
1154 'args': args
1155 })
1156
1157 script_name = gmTools.get_unique_filename(prefix = 'gm-assimilate-%(pat2del)s-into-%(pat2keep)s-' % args, suffix = '.sql')
1158 _log.warning('identity [%s] is about to assimilate identity [%s], SQL script [%s]', self.ID, other_identity.ID, script_name)
1159
1160 script = io.open(script_name, 'wt')
1161 args['date'] = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %B %d %H:%M')
1162 script.write(_MERGE_SCRIPT_HEADER % args)
1163 for query in queries:
1164 script.write(query['cmd'] % args)
1165 script.write(';\n')
1166 script.write('\nROLLBACK;\n')
1167 script.write('--COMMIT;\n')
1168 script.close()
1169
1170 try:
1171 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
1172 except Exception:
1173 return False, _('The merge failed. Check the log and [%s]') % script_name
1174
1175 self.add_external_id (
1176 type_name = 'merged GNUmed identity primary key',
1177 value = 'GNUmed::pk::%s' % other_identity.ID,
1178 issuer = 'GNUmed'
1179 )
1180
1181 return True, None
1182
1183
1184
1186 cmd = """
1187 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
1188 values (
1189 %(pat)s,
1190 %(urg)s,
1191 %(cmt)s,
1192 %(area)s,
1193 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
1194 )"""
1195 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
1196 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
1197 gmHooks.run_hook_script(hook = 'after_waiting_list_modified')
1198
1199
1201 cmd = """SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
1202 args = {'pat': self.ID}
1203 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1204 return rows
1205
1206 waiting_list_entries = property(get_waiting_list_entry, lambda x:x)
1207
1208
1211
1212 export_area = property(_get_export_area, lambda x:x)
1213
1214 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
1215
1216 template = '%s%s%s\r\n'
1217
1218 if filename is None:
1219 filename = gmTools.get_unique_filename (
1220 prefix = 'gm-patient-',
1221 suffix = '.gdt'
1222 )
1223
1224 gdt_file = io.open(filename, mode = 'wt', encoding = encoding, errors = 'strict')
1225
1226 gdt_file.write(template % ('013', '8000', '6301'))
1227 gdt_file.write(template % ('013', '9218', '2.10'))
1228 if external_id_type is None:
1229 gdt_file.write(template % ('%03d' % (9 + len(str(self.ID))), '3000', self.ID))
1230 else:
1231 ext_ids = self.get_external_ids(id_type = external_id_type)
1232 if len(ext_ids) > 0:
1233 gdt_file.write(template % ('%03d' % (9 + len(ext_ids[0]['value'])), '3000', ext_ids[0]['value']))
1234 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['lastnames']])), '3101', self._payload[self._idx['lastnames']]))
1235 gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['firstnames']])), '3102', self._payload[self._idx['firstnames']]))
1236 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')))
1237 gdt_file.write(template % ('010', '3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
1238 gdt_file.write(template % ('025', '6330', 'GNUmed::9206::encoding'))
1239 gdt_file.write(template % ('%03d' % (9 + len(encoding)), '6331', encoding))
1240 if external_id_type is None:
1241 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
1242 gdt_file.write(template % ('017', '6333', 'internal'))
1243 else:
1244 if len(ext_ids) > 0:
1245 gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
1246 gdt_file.write(template % ('%03d' % (9 + len(external_id_type)), '6333', external_id_type))
1247
1248 gdt_file.close()
1249
1250 return filename
1251
1253
1254 if filename is None:
1255 filename = gmTools.get_unique_filename (
1256 prefix = 'gm-LinuxMedNews_demographics-',
1257 suffix = '.xml'
1258 )
1259
1260 dob_format = '%Y-%m-%d'
1261 pat = etree.Element('patient')
1262
1263 first = etree.SubElement(pat, 'firstname')
1264 first.text = gmTools.coalesce(self._payload[self._idx['firstnames']], '')
1265
1266 last = etree.SubElement(pat, 'lastname')
1267 last.text = gmTools.coalesce(self._payload[self._idx['lastnames']], '')
1268
1269
1270
1271
1272
1273
1274 pref = etree.SubElement(pat, 'name_prefix')
1275 pref.text = gmTools.coalesce(self._payload[self._idx['title']], '')
1276
1277 suff = etree.SubElement(pat, 'name_suffix')
1278 suff.text = ''
1279
1280 dob = etree.SubElement(pat, 'DOB')
1281 dob.set('format', dob_format)
1282 dob.text = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
1283
1284 gender = etree.SubElement(pat, 'gender')
1285 gender.set('comment', self.gender_string)
1286 if self._payload[self._idx['gender']] is None:
1287 gender.text = ''
1288 else:
1289 gender.text = map_gender2mf[self._payload[self._idx['gender']]]
1290
1291 home = etree.SubElement(pat, 'home_address')
1292 adrs = self.get_addresses(address_type = 'home')
1293 if len(adrs) > 0:
1294 adr = adrs[0]
1295 city = etree.SubElement(home, 'city')
1296 city.set('comment', gmTools.coalesce(adr['suburb'], ''))
1297 city.text = gmTools.coalesce(adr['urb'], '')
1298
1299 region = etree.SubElement(home, 'region')
1300 region.set('comment', gmTools.coalesce(adr['l10n_region'], ''))
1301 region.text = gmTools.coalesce(adr['code_region'], '')
1302
1303 zipcode = etree.SubElement(home, 'postal_code')
1304 zipcode.text = gmTools.coalesce(adr['postcode'], '')
1305
1306 street = etree.SubElement(home, 'street')
1307 street.set('comment', gmTools.coalesce(adr['notes_street'], ''))
1308 street.text = gmTools.coalesce(adr['street'], '')
1309
1310 no = etree.SubElement(home, 'number')
1311 no.set('subunit', gmTools.coalesce(adr['subunit'], ''))
1312 no.set('comment', gmTools.coalesce(adr['notes_subunit'], ''))
1313 no.text = gmTools.coalesce(adr['number'], '')
1314
1315 country = etree.SubElement(home, 'country')
1316 country.set('comment', adr['l10n_country'])
1317 country.text = gmTools.coalesce(adr['code_country'], '')
1318
1319 phone = etree.SubElement(pat, 'home_phone')
1320 rec = self.get_comm_channels(comm_medium = 'homephone')
1321 if len(rec) > 0:
1322 if not rec[0]['is_confidential']:
1323 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1324 phone.text = rec[0]['url']
1325
1326 phone = etree.SubElement(pat, 'work_phone')
1327 rec = self.get_comm_channels(comm_medium = 'workphone')
1328 if len(rec) > 0:
1329 if not rec[0]['is_confidential']:
1330 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1331 phone.text = rec[0]['url']
1332
1333 phone = etree.SubElement(pat, 'cell_phone')
1334 rec = self.get_comm_channels(comm_medium = 'mobile')
1335 if len(rec) > 0:
1336 if not rec[0]['is_confidential']:
1337 phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
1338 phone.text = rec[0]['url']
1339
1340 tree = etree.ElementTree(pat)
1341 tree.write(filename, encoding = 'UTF-8')
1342
1343 return filename
1344
1345
1347
1348
1349
1350
1351
1352 dob_format = '%Y%m%d'
1353
1354 import vobject
1355
1356 vc = vobject.vCard()
1357 vc.add('kind')
1358 vc.kind.value = 'individual'
1359
1360 vc.add('fn')
1361 vc.fn.value = self.get_description(with_nickname = False)
1362 vc.add('n')
1363 vc.n.value = vobject.vcard.Name(family = self._payload[self._idx['lastnames']], given = self._payload[self._idx['firstnames']])
1364
1365
1366
1367 vc.add('title')
1368 vc.title.value = gmTools.coalesce(self._payload[self._idx['title']], '')
1369 vc.add('gender')
1370
1371 vc.gender.value = map_gender2vcard[self._payload[self._idx['gender']]]
1372 vc.add('bday')
1373 vc.bday.value = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
1374
1375 channels = self.get_comm_channels(comm_medium = 'homephone')
1376 if len(channels) > 0:
1377 if not channels[0]['is_confidential']:
1378 vc.add('tel')
1379 vc.tel.value = channels[0]['url']
1380 vc.tel.type_param = 'HOME'
1381 channels = self.get_comm_channels(comm_medium = 'workphone')
1382 if len(channels) > 0:
1383 if not channels[0]['is_confidential']:
1384 vc.add('tel')
1385 vc.tel.value = channels[0]['url']
1386 vc.tel.type_param = 'WORK'
1387 channels = self.get_comm_channels(comm_medium = 'mobile')
1388 if len(channels) > 0:
1389 if not channels[0]['is_confidential']:
1390 vc.add('tel')
1391 vc.tel.value = channels[0]['url']
1392 vc.tel.type_param = 'CELL'
1393 channels = self.get_comm_channels(comm_medium = 'fax')
1394 if len(channels) > 0:
1395 if not channels[0]['is_confidential']:
1396 vc.add('tel')
1397 vc.tel.value = channels[0]['url']
1398 vc.tel.type_param = 'FAX'
1399 channels = self.get_comm_channels(comm_medium = 'email')
1400 if len(channels) > 0:
1401 if not channels[0]['is_confidential']:
1402 vc.add('email')
1403 vc.tel.value = channels[0]['url']
1404 vc.tel.type_param = 'INTERNET'
1405 channels = self.get_comm_channels(comm_medium = 'web')
1406 if len(channels) > 0:
1407 if not channels[0]['is_confidential']:
1408 vc.add('url')
1409 vc.tel.value = channels[0]['url']
1410 vc.tel.type_param = 'INTERNET'
1411
1412 adrs = self.get_addresses(address_type = 'home')
1413 if len(adrs) > 0:
1414 home_adr = adrs[0]
1415 vc.add('adr')
1416 vc.adr.type_param = 'HOME'
1417 vc.adr.value = vobject.vcard.Address()
1418 vc_adr = vc.adr.value
1419 vc_adr.extended = gmTools.coalesce(home_adr['subunit'], '')
1420 vc_adr.street = gmTools.coalesce(home_adr['street'], '', '%s ') + gmTools.coalesce(home_adr['number'], '')
1421 vc_adr.region = gmTools.coalesce(home_adr['l10n_region'], '')
1422 vc_adr.code = gmTools.coalesce(home_adr['postcode'], '')
1423 vc_adr.city = gmTools.coalesce(home_adr['urb'], '')
1424 vc_adr.country = gmTools.coalesce(home_adr['l10n_country'], '')
1425
1426
1427
1428 if filename is None:
1429 filename = gmTools.get_unique_filename (
1430 prefix = 'gm-patient-',
1431 suffix = '.vcf'
1432 )
1433 vcf = io.open(filename, mode = 'wt', encoding = 'utf8')
1434 try:
1435 vcf.write(vc.serialize())
1436 except UnicodeDecodeError:
1437 _log.exception('failed to serialize VCF data')
1438 vcf.close()
1439 return 'cannot-serialize.vcf'
1440 vcf.close()
1441
1442 return filename
1443
1444
1454
1455
1457 """
1458 http://blog.thenetimpact.com/2011/07/decoding-qr-codes-how-to-format-data-for-qr-code-generators/
1459 https://www.nttdocomo.co.jp/english/service/developer/make/content/barcode/function/application/addressbook/index.html
1460
1461 MECARD:N:NAME;ADR:pobox,subunit,unit,street,ort,region,zip,country;TEL:111111111;FAX:22222222;EMAIL:mail@praxis.org;
1462
1463
1464 MECARD:N:lastname,firstname;BDAY:YYYYMMDD;ADR:pobox,subunit,number,street,location,region,zip,country;;
1465 MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR:,$<adr_subunit::home::>$,$<adr_number::home::>$,$<adr_street::home::>$,$<adr_location::home::>$,,$<adr_postcode::home::>$,$<adr_country::home::>$;;
1466 """
1467 MECARD = 'MECARD:N:%s,%s;' % (
1468 self._payload[self._idx['lastnames']],
1469 self._payload[self._idx['firstnames']]
1470 )
1471 if self._payload[self._idx['dob']] is not None:
1472 MECARD += 'BDAY:%s;' % gmDateTime.pydt_strftime (
1473 self._payload[self._idx['dob']],
1474 '%Y%m%d',
1475 accuracy = gmDateTime.acc_days,
1476 none_str = ''
1477 )
1478 adrs = self.get_addresses(address_type = 'home')
1479 if len(adrs) > 0:
1480 MECARD += 'ADR:,%(subunit)s,%(number)s,%(street)s,%(urb)s,,%(postcode)s,%(l10n_country)s;' % adrs[0]
1481 comms = self.get_comm_channels(comm_medium = 'homephone')
1482 if len(comms) > 0:
1483 if not comms[0]['is_confidential']:
1484 MECARD += 'TEL:%s;' % comms[0]['url']
1485 comms = self.get_comm_channels(comm_medium = 'fax')
1486 if len(comms) > 0:
1487 if not comms[0]['is_confidential']:
1488 MECARD += 'FAX:%s;' % comms[0]['url']
1489 comms = self.get_comm_channels(comm_medium = 'email')
1490 if len(comms) > 0:
1491 if not comms[0]['is_confidential']:
1492 MECARD += 'EMAIL:%s;' % comms[0]['url']
1493 return MECARD
1494
1495 MECARD = property(_get_mecard)
1496
1497
1498
1499
1502
1503
1505 """Link an occupation with a patient, creating the occupation if it does not exists.
1506
1507 @param occupation The name of the occupation to link the patient to.
1508 """
1509 if (activities is None) and (occupation is None):
1510 return True
1511
1512 occupation = occupation.strip()
1513 if len(occupation) == 0:
1514 return True
1515
1516 if activities is not None:
1517 activities = activities.strip()
1518
1519 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
1520
1521 cmd = "select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
1522 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1523
1524 queries = []
1525 if len(rows) == 0:
1526 queries.append ({
1527 'cmd': "INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
1528 'args': args
1529 })
1530 else:
1531 if rows[0]['activities'] != activities:
1532 queries.append ({
1533 '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))",
1534 'args': args
1535 })
1536
1537 rows, idx = gmPG2.run_rw_queries(queries = queries)
1538
1539 return True
1540
1542 if occupation is None:
1543 return True
1544 occupation = occupation.strip()
1545 cmd = "delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
1546 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
1547 return True
1548
1549
1550
1552 cmd = "select * from dem.v_person_comms where pk_identity = %s"
1553 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
1554
1555 filtered = rows
1556
1557 if comm_medium is not None:
1558 filtered = []
1559 for row in rows:
1560 if row['comm_type'] == comm_medium:
1561 filtered.append(row)
1562
1563 return [ gmDemographicRecord.cCommChannel(row = {
1564 'pk_field': 'pk_lnk_identity2comm',
1565 'data': r,
1566 'idx': idx
1567 }) for r in filtered
1568 ]
1569
1570 comm_channels = property(get_comm_channels, lambda x:x)
1571
1572 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
1573 """Link a communication medium with a patient.
1574
1575 @param comm_medium The name of the communication medium.
1576 @param url The communication resource locator.
1577 @type url A str instance.
1578 @param is_confidential Wether the data must be treated as confidential.
1579 @type is_confidential A bool instance.
1580 """
1581 comm_channel = gmDemographicRecord.create_comm_channel (
1582 comm_medium = comm_medium,
1583 url = url,
1584 is_confidential = is_confidential,
1585 pk_channel_type = pk_channel_type,
1586 pk_identity = self.pk_obj
1587 )
1588 return comm_channel
1589
1595
1596
1597
1599 cmd = "SELECT pk_address FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
1600 args = {'pat': self.pk_obj}
1601 if address_type is not None:
1602 cmd = cmd + " AND address_type = %(typ)s"
1603 args['typ'] = address_type
1604
1605 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1606 return [
1607 gmDemographicRecord.cPatientAddress(aPK_obj = {
1608 'pk_adr': r['pk_address'],
1609 'pk_pat': self.pk_obj
1610 }) for r in rows
1611 ]
1612
1613
1614 - 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):
1615 """Link an address with a patient, creating the address if it does not exists.
1616
1617 @param id_type The primary key of the address type.
1618 """
1619 if address is None:
1620 address = gmDemographicRecord.create_address (
1621 country_code = country_code,
1622 region_code = region_code,
1623 urb = urb,
1624 suburb = suburb,
1625 postcode = postcode,
1626 street = street,
1627 number = number,
1628 subunit = subunit
1629 )
1630 if address is None:
1631 return None
1632
1633
1634 cmd = "SELECT id_address FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
1635 args = {'pat': self.pk_obj, 'adr': address['pk_address']}
1636 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
1637
1638 if not rows:
1639 args = {'pk_person': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
1640 cmd = """
1641 INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
1642 VALUES (%(pk_person)s, %(adr)s)
1643 RETURNING id_address"""
1644 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
1645 pk_data = {
1646 'pk_adr': rows[0]['id_address'],
1647 'pk_pat': self.pk_obj
1648 }
1649 linked_adr = gmDemographicRecord.cPatientAddress(aPK_obj = pk_data)
1650
1651 if id_type is None:
1652 if adr_type is not None:
1653 id_type = gmDemographicRecord.create_address_type(address_type = adr_type)
1654 if id_type is not None:
1655 linked_adr['pk_address_type'] = id_type
1656 linked_adr.save()
1657 return linked_adr
1658
1659
1661 """Remove an address from the patient.
1662
1663 The address itself stays in the database.
1664 The address can be either cAdress or cPatientAdress.
1665 """
1666 if pk_address is None:
1667 args = {'person': self.pk_obj, 'adr': address['pk_address']}
1668 else:
1669 args = {'person': self.pk_obj, 'adr': pk_address}
1670 cmd = """
1671 DELETE FROM dem.lnk_person_org_address
1672 WHERE
1673 dem.lnk_person_org_address.id_identity = %(person)s
1674 AND
1675 dem.lnk_person_org_address.id_address = %(adr)s
1676 AND
1677 NOT EXISTS(SELECT 1 FROM bill.bill WHERE fk_receiver_address = dem.lnk_person_org_address.id)
1678 """
1679 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1680
1681
1682
1683 - def get_bills(self, order_by=None, pk_patient=None):
1684 return gmBilling.get_bills (
1685 order_by = order_by,
1686 pk_patient = self.pk_obj
1687 )
1688
1689 bills = property(get_bills, lambda x:x)
1690
1691
1692
1694 cmd = """
1695 SELECT
1696 d_rt.description,
1697 d_vap.*
1698 FROM
1699 dem.v_all_persons d_vap,
1700 dem.relation_types d_rt,
1701 dem.lnk_person2relative d_lp2r
1702 WHERE
1703 ( d_lp2r.id_identity = %(pk)s
1704 AND
1705 d_vap.pk_identity = d_lp2r.id_relative
1706 AND
1707 d_rt.id = d_lp2r.id_relation_type
1708 ) or (
1709 d_lp2r.id_relative = %(pk)s
1710 AND
1711 d_vap.pk_identity = d_lp2r.id_identity
1712 AND
1713 d_rt.inverse = d_lp2r.id_relation_type
1714 )"""
1715 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
1716 if len(rows) == 0:
1717 return []
1718 return [(row[0], cPerson(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk_identity'})) for row in rows]
1719
1721
1722 id_new_relative = create_dummy_identity()
1723
1724 relative = cPerson(aPK_obj=id_new_relative)
1725
1726
1727 relative.add_name( '**?**', self.get_names()['lastnames'])
1728
1729 if 'relatives' in self._ext_cache:
1730 del self._ext_cache['relatives']
1731 cmd = """
1732 insert into dem.lnk_person2relative (
1733 id_identity, id_relative, id_relation_type
1734 ) values (
1735 %s, %s, (select id from dem.relation_types where description = %s)
1736 )"""
1737 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type ]}])
1738 return True
1739
1741
1742 self.set_relative(None, relation)
1743
1748
1749 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1750
1751
1752
1753
1761
1762
1803
1804
1805 - def dob_in_range(self, min_distance='1 week', max_distance='1 week'):
1806 if self['dob'] is None:
1807 return False
1808 cmd = 'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1809 rows, idx = gmPG2.run_ro_queries (
1810 queries = [{
1811 'cmd': cmd,
1812 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1813 }]
1814 )
1815 return rows[0][0]
1816
1817
1819 if self['dob'] is None:
1820 return None
1821 now = gmDateTime.pydt_now_here()
1822 if now.month < self['dob'].month:
1823 return False
1824 if now.month > self['dob'].month:
1825 return True
1826
1827 if now.day < self['dob'].day:
1828 return False
1829 if now.day > self['dob'].day:
1830 return True
1831
1832 return False
1833
1834 current_birthday_passed = property(_get_current_birthday_passed)
1835
1836
1846
1847 birthday_this_year = property(_get_birthday_this_year)
1848
1849
1859
1860 birthday_next_year = property(_get_birthday_next_year)
1861
1862
1872
1873 birthday_last_year = property(_get_birthday_last_year, lambda x:x)
1874
1875
1876
1877
1879 cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
1880 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1881 if len(rows) > 0:
1882 return rows[0]
1883 else:
1884 return None
1885
1888
1889 messages = property(get_messages, lambda x:x)
1890
1893
1894 overdue_messages = property(_get_overdue_messages, lambda x:x)
1895
1896
1899
1900
1906
1907 dynamic_hints = property(_get_dynamic_hints, lambda x:x)
1908
1909
1912
1913 suppressed_hints = property(_get_suppressed_hints, lambda x:x)
1914
1915
1917 if self._payload[self._idx['pk_primary_provider']] is None:
1918 return None
1919 cmd = "SELECT * FROM dem.v_all_persons WHERE pk_identity = (SELECT pk_identity FROM dem.v_staff WHERE pk_staff = %(pk_staff)s)"
1920 args = {'pk_staff': self._payload[self._idx['pk_primary_provider']]}
1921 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1922 if len(rows) == 0:
1923 return None
1924 return cPerson(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_identity'})
1925
1926 primary_provider_identity = property(_get_primary_provider_identity, lambda x:x)
1927
1928
1930 if self._payload[self._idx['pk_primary_provider']] is None:
1931 return None
1932 from Gnumed.business import gmStaff
1933 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1934
1935 primary_provider = property(_get_primary_provider, lambda x:x)
1936
1937
1938
1939
1941 """Format patient demographics into patient specific path name fragment."""
1942
1943 return gmTools.fname_sanitize('%s-%s-%s-ID_%s' % (
1944 self._payload[self._idx['lastnames']],
1945 self._payload[self._idx['firstnames']],
1946 self.get_formatted_dob(format = '%Y-%m-%d'),
1947 self._payload[self._idx['pk_identity']]
1948 ))
1949
1950
1951
1952
1953 subdir_name = property(get_subdir_name, lambda x:x)
1954
1955
1957 cmd = 'SELECT 1 FROM clin.patient WHERE fk_identity = %(pk_pat)s'
1958 args = {'pk_pat': pk_identity}
1959 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
1960 if len(rows) == 0:
1961 return False
1962 return True
1963
1964
1966 cmd = """
1967 INSERT INTO clin.patient (fk_identity)
1968 SELECT %(pk_ident)s WHERE NOT EXISTS (
1969 SELECT 1 FROM clin.patient c_p WHERE fk_identity = %(pk_ident)s
1970 )"""
1971 args = {'pk_ident': pk_identity}
1972 queries = [{'cmd': cmd, 'args': args}]
1973 gmPG2.run_rw_queries(queries = queries)
1974 return True
1975
1976
1977
1978
1979 _yield = lambda *x:None
1980
1982 if not callable(yielder):
1983 raise TypeError('yielder <%s> is not callable' % yielder)
1984 global _yield
1985 _yield = yielder
1986 _log.debug('setting yielder to <%s>', yielder)
1987
1988
1990 """Represents a person which is a patient.
1991
1992 - a specializing subclass of cPerson turning it into a patient
1993 - its use is to cache subobjects like EMR and document folder
1994 """
1995 - def __init__(self, aPK_obj=None, row=None):
1996 cPerson.__init__(self, aPK_obj = aPK_obj, row = row)
1997 self.__emr_access_lock = threading.Lock()
1998 self.__emr = None
1999 self.__doc_folder = None
2000
2001
2003 """Do cleanups before dying.
2004
2005 - note that this may be called in a thread
2006 """
2007 if self.__emr is not None:
2008 self.__emr.cleanup()
2009 if self.__doc_folder is not None:
2010 self.__doc_folder.cleanup()
2011 cPerson.cleanup(self)
2012
2013
2018
2019
2021 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
2022
2023
2024 if self.__emr is not None:
2025 return self.__emr
2026
2027 stack_logged = False
2028 got_lock = self.__emr_access_lock.acquire(False)
2029 if not got_lock:
2030
2031 call_stack = inspect.stack()
2032 call_stack.reverse()
2033 for idx in range(1, len(call_stack)):
2034 caller = call_stack[idx]
2035 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
2036 del call_stack
2037 stack_logged = True
2038
2039 for idx in range(500):
2040 _yield()
2041 time.sleep(0.1)
2042 _yield()
2043 got_lock = self.__emr_access_lock.acquire(False)
2044 if got_lock:
2045 break
2046 if not got_lock:
2047 _log.error('still failed to acquire EMR access lock, aborting (thread [%s])', threading.get_ident())
2048 self.__emr_access_lock.release()
2049 raise AttributeError('cannot lock access to EMR for identity [%s]' % self._payload[self._idx['pk_identity']])
2050
2051 _log.debug('pulling chart for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
2052 if not stack_logged:
2053
2054 call_stack = inspect.stack()
2055 call_stack.reverse()
2056 for idx in range(1, len(call_stack)):
2057 caller = call_stack[idx]
2058 _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
2059 del call_stack
2060 stack_logged = True
2061
2062 self.is_patient = True
2063 from Gnumed.business import gmClinicalRecord
2064 emr = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
2065
2066 _log.debug('returning EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
2067 self.__emr = emr
2068 self.__emr_access_lock.release()
2069 return self.__emr
2070
2071 emr = property(get_emr, lambda x:x)
2072
2073
2075 if self.__doc_folder is None:
2076 self.__doc_folder = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
2077 return self.__doc_folder
2078
2079 document_folder = property(get_document_folder, lambda x:x)
2080
2081
2083 """Patient Borg to hold the currently active patient.
2084
2085 There may be many instances of this but they all share state.
2086
2087 The underlying dem.identity row must have .deleted set to FALSE.
2088
2089 The sequence of events when changing the active patient:
2090
2091 1) Registered callbacks are run.
2092 Those are run synchronously. If a callback
2093 returns False or throws an exception the
2094 patient switch is aborted. Callback code
2095 can rely on the patient still being active
2096 and to not go away until it returns. It
2097 is not passed any arguments and must return
2098 False or True.
2099
2100 2) Signal "pre_patient_unselection" is sent.
2101 This does not wait for nor check results.
2102 The keyword pk_identity contains the
2103 PK of the person being switched away
2104 from.
2105
2106 3) the current patient is unset (gmNull.cNull)
2107
2108 4) Signal "current_patient_unset" is sent
2109 At this point resetting GUI fields to
2110 empty should be done. The active patient
2111 is not there anymore.
2112
2113 This does not wait for nor check results.
2114
2115 5) The current patient is set to the new value.
2116 The new patient can also remain gmNull.cNull
2117 in case the calling code explicitely unset
2118 the current patient.
2119
2120 6) Signal "post_patient_selection" is sent.
2121 Code listening to this signal can
2122 assume that the new patient is
2123 already active.
2124 """
2125 - def __init__(self, patient=None, forced_reload=False):
2126 """Change or get currently active patient.
2127
2128 patient:
2129 * None: get currently active patient
2130 * -1: unset currently active patient
2131 * cPatient instance: set active patient if possible
2132 """
2133
2134 try:
2135 self.patient
2136 except AttributeError:
2137 self.patient = gmNull.cNull()
2138 self.__register_interests()
2139
2140
2141
2142 self.__lock_depth = 0
2143
2144 self.__callbacks_before_switching_away_from_patient = []
2145
2146
2147 if patient is None:
2148 return None
2149
2150
2151 if self.locked:
2152 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
2153 return None
2154
2155
2156 if patient == -1:
2157 _log.debug('explicitly unsetting current patient')
2158 if not self.__run_callbacks_before_switching_away_from_patient():
2159 _log.error('not unsetting current patient, at least one pre-change callback failed')
2160 return None
2161 self.__send_pre_unselection_notification()
2162 self.patient.cleanup()
2163 self.patient = gmNull.cNull()
2164 self.__send_unselection_notification()
2165
2166 time.sleep(0.5)
2167 self.__send_selection_notification()
2168 return None
2169
2170
2171 if not isinstance(patient, cPatient):
2172 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
2173 raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
2174
2175
2176 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
2177 return None
2178
2179 if patient['is_deleted']:
2180 _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
2181 raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
2182
2183
2184 _log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
2185
2186 if not self.__run_callbacks_before_switching_away_from_patient():
2187 _log.error('not changing current patient, at least one pre-change callback failed')
2188 return None
2189
2190
2191 self.__send_pre_unselection_notification()
2192 self.patient.cleanup()
2193 self.patient = gmNull.cNull()
2194 self.__send_unselection_notification()
2195
2196 time.sleep(0.5)
2197 self.patient = patient
2198
2199
2200 self.patient.emr
2201 self.__send_selection_notification()
2202
2203 return None
2204
2205
2208
2209
2211
2212 if isinstance(self.patient, gmNull.cNull):
2213 return True
2214
2215
2216 if kwds['table'] not in ['dem.identity', 'dem.names']:
2217 return True
2218
2219
2220 if int(kwds['pk_identity']) != self.patient.ID:
2221 return True
2222
2223 if kwds['table'] == 'dem.identity':
2224
2225 if kwds['operation'] != 'UPDATE':
2226 return True
2227
2228 self.patient.refetch_payload()
2229 return True
2230
2231
2232
2233
2235
2236
2237
2238
2239
2240 if not callable(callback):
2241 raise TypeError('callback [%s] not callable' % callback)
2242
2243 self.__callbacks_before_switching_away_from_patient.append(callback)
2244
2245
2248
2249 connected = property(_get_connected, lambda x:x)
2250
2251
2253 return (self.__lock_depth > 0)
2254
2256 if locked:
2257 self.__lock_depth = self.__lock_depth + 1
2258 gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__)
2259 else:
2260 if self.__lock_depth == 0:
2261 _log.error('lock/unlock imbalance, tried to refcount lock depth below 0')
2262 return
2263 else:
2264 self.__lock_depth = self.__lock_depth - 1
2265 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2266
2267 locked = property(_get_locked, _set_locked)
2268
2269
2271 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
2272 self.__lock_depth = 0
2273 gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
2274
2275
2276
2277
2279 if isinstance(self.patient, gmNull.cNull):
2280 return True
2281
2282 for call_back in self.__callbacks_before_switching_away_from_patient:
2283 try:
2284 successful = call_back()
2285 except Exception:
2286 _log.exception('callback [%s] failed', call_back)
2287 print("*** pre-change callback failed ***")
2288 print(type(call_back))
2289 print(call_back)
2290 return False
2291
2292 if not successful:
2293 _log.error('callback [%s] returned False', call_back)
2294 return False
2295
2296 return True
2297
2298
2300 """Sends signal when current patient is about to be unset.
2301
2302 This does NOT wait for signal handlers to complete.
2303 """
2304 kwargs = {
2305 'signal': 'pre_patient_unselection',
2306 'sender': self.__class__.__name__,
2307 'pk_identity': self.patient['pk_identity']
2308 }
2309 gmDispatcher.send(**kwargs)
2310
2311
2313 """Sends signal when the previously active patient has
2314 been unset during a change of active patient.
2315
2316 This is the time to initialize GUI fields to empty values.
2317
2318 This does NOT wait for signal handlers to complete.
2319 """
2320 kwargs = {
2321 'signal': 'current_patient_unset',
2322 'sender': self.__class__.__name__
2323 }
2324 gmDispatcher.send(**kwargs)
2325
2326
2328 """Sends signal when another patient has actually been made active."""
2329 kwargs = {
2330 'signal': 'post_patient_selection',
2331 'sender': self.__class__.__name__,
2332 'pk_identity': self.patient['pk_identity'],
2333 'current_patient': self
2334 }
2335 gmDispatcher.send(**kwargs)
2336
2337
2338
2339
2341
2342
2343
2344
2345
2346
2347 if attribute == 'patient':
2348 raise AttributeError
2349 if isinstance(self.patient, gmNull.cNull):
2350 _log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute)
2351 raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute))
2352 return getattr(self.patient, attribute)
2353
2354
2355
2356
2358 """Return any attribute if known how to retrieve it by proxy.
2359 """
2360 return self.patient[attribute]
2361
2362
2365
2366
2367
2368
2371 gmMatchProvider.cMatchProvider_SQL2.__init__(
2372 self,
2373 queries = [
2374 """SELECT
2375 pk_staff AS data,
2376 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
2377 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
2378 FROM dem.v_staff
2379 WHERE
2380 is_active AND (
2381 short_alias %(fragment_condition)s OR
2382 firstnames %(fragment_condition)s OR
2383 lastnames %(fragment_condition)s OR
2384 db_user %(fragment_condition)s
2385 )
2386 """
2387 ]
2388 )
2389 self.setThresholds(1, 2, 3)
2390
2391
2392
2393
2394 -def create_name(pk_person, firstnames, lastnames, active=False):
2395 queries = [{
2396 'cmd': "select dem.add_name(%s, %s, %s, %s)",
2397 'args': [pk_person, firstnames, lastnames, active]
2398 }]
2399 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
2400 name = cPersonName(aPK_obj = rows[0][0])
2401 return name
2402
2403
2404 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None, comment=None):
2405
2406 cmd1 = "INSERT INTO dem.identity (gender, dob, comment) VALUES (%s, %s, %s)"
2407 cmd2 = """
2408 INSERT INTO dem.names (
2409 id_identity, lastnames, firstnames
2410 ) VALUES (
2411 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
2412 ) RETURNING id_identity"""
2413
2414 try:
2415 rows, idx = gmPG2.run_rw_queries (
2416 queries = [
2417 {'cmd': cmd1, 'args': [gender, dob, comment]},
2418 {'cmd': cmd2, 'args': [lastnames, firstnames]}
2419
2420 ],
2421 return_data = True
2422 )
2423 except Exception:
2424 _log.exception('cannot create identity')
2425 gmLog2.log_stack_trace()
2426 return None
2427 ident = cPerson(aPK_obj = rows[0][0])
2428 gmHooks.run_hook_script(hook = 'post_person_creation')
2429 return ident
2430
2431
2433 _log.info('disabling identity [%s]', pk_identity)
2434 cmd = "UPDATE dem.identity SET deleted = true WHERE pk = %(pk)s"
2435 args = {'pk': pk_identity}
2436 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2437 return True
2438
2439
2447
2448
2450 cmd = 'SELECT EXISTS(SELECT 1 FROM dem.identity where pk = %(pk)s)'
2451 args = {'pk': pk_identity}
2452 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
2453 return rows[0][0]
2454
2455
2457 """Set active patient.
2458
2459 If patient is -1 the active patient will be UNset.
2460 """
2461 if isinstance(patient, gmCurrentPatient):
2462 return True
2463
2464 if isinstance(patient, cPatient):
2465 pat = patient
2466 elif isinstance(patient, cPerson):
2467 pat = pat.as_patient
2468 elif patient == -1:
2469 pat = patient
2470 else:
2471
2472 success, pk = gmTools.input2int(initial = patient, minval = 1)
2473 if not success:
2474 raise ValueError('<patient> must be either -1, >0, or a cPatient, cPerson or gmCurrentPatient instance, is: %s' % patient)
2475
2476 try:
2477 pat = cPatient(aPK_obj = pk)
2478 except Exception:
2479 _log.exception('identity [%s] not found' % patient)
2480 return False
2481
2482
2483 try:
2484 gmCurrentPatient(patient = pat, forced_reload = forced_reload)
2485 except Exception:
2486 _log.exception('error changing active patient to [%s]' % patient)
2487 return False
2488
2489 return True
2490
2491
2492
2493
2505
2506
2507 map_gender2mf = {
2508 'm': 'm',
2509 'f': 'f',
2510 'tf': 'f',
2511 'tm': 'm',
2512 'h': 'mf'
2513 }
2514
2515
2516
2517 map_gender2vcard = {
2518 'm': 'M',
2519 'f': 'F',
2520 'tf': 'F',
2521 'tm': 'M',
2522 'h': 'O',
2523 None: 'U'
2524 }
2525
2526
2527
2528 map_gender2symbol = {
2529 'm': '\u2642',
2530 'f': '\u2640',
2531 'tf': '\u26A5\u2640',
2532
2533 'tm': '\u26A5\u2642',
2534
2535 'h': '\u26A5',
2536
2537 None: '?\u26A5?'
2538 }
2539
2561
2584
2586 """Try getting the gender for the given first name."""
2587
2588 if firstnames is None:
2589 return None
2590
2591 rows, idx = gmPG2.run_ro_queries(queries = [{
2592 'cmd': "SELECT gender FROM dem.name_gender_map WHERE name ILIKE %(fn)s LIMIT 1",
2593 'args': {'fn': firstnames}
2594 }])
2595
2596 if len(rows) == 0:
2597 return None
2598
2599 return rows[0][0]
2600
2602 cmd = 'SELECT pk FROM dem.identity'
2603 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
2604 return [ r[0] for r in rows ]
2605
2606
2608 return [ cPerson(aPK_obj = pk) for pk in pks ]
2609
2610
2614
2615
2619
2620
2621
2622
2623 if __name__ == '__main__':
2624
2625 if len(sys.argv) == 1:
2626 sys.exit()
2627
2628 if sys.argv[1] != 'test':
2629 sys.exit()
2630
2631 import datetime
2632
2633 gmI18N.activate_locale()
2634 gmI18N.install_domain()
2635 gmDateTime.init()
2636
2637
2658
2660 dto = cDTO_person()
2661 dto.firstnames = 'Sepp'
2662 dto.lastnames = 'Herberger'
2663 dto.gender = 'male'
2664 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
2665 print(dto)
2666
2667 print(dto['firstnames'])
2668 print(dto['lastnames'])
2669 print(dto['gender'])
2670 print(dto['dob'])
2671
2672 for key in dto.keys():
2673 print(key)
2674
2676
2677 print('\n\nCreating identity...')
2678 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
2679 print('Identity created: %s' % new_identity)
2680
2681 print('\nSetting title and gender...')
2682 new_identity['title'] = 'test title';
2683 new_identity['gender'] = 'f';
2684 new_identity.save_payload()
2685 print('Refetching identity from db: %s' % cPerson(aPK_obj=new_identity['pk_identity']))
2686
2687 print('\nGetting all names...')
2688 for a_name in new_identity.get_names():
2689 print(a_name)
2690 print('Active name: %s' % (new_identity.get_active_name()))
2691 print('Setting nickname...')
2692 new_identity.set_nickname(nickname='test nickname')
2693 print('Refetching all names...')
2694 for a_name in new_identity.get_names():
2695 print(a_name)
2696 print('Active name: %s' % (new_identity.get_active_name()))
2697
2698 print('\nIdentity occupations: %s' % new_identity['occupations'])
2699 print('Creating identity occupation...')
2700 new_identity.link_occupation('test occupation')
2701 print('Identity occupations: %s' % new_identity['occupations'])
2702
2703 print('\nIdentity addresses: %s' % new_identity.get_addresses())
2704 print('Creating identity address...')
2705
2706 new_identity.link_address (
2707 number = 'test 1234',
2708 street = 'test street',
2709 postcode = 'test postcode',
2710 urb = 'test urb',
2711 region_code = 'SN',
2712 country_code = 'DE'
2713 )
2714 print('Identity addresses: %s' % new_identity.get_addresses())
2715
2716 print('\nIdentity communications: %s' % new_identity.get_comm_channels())
2717 print('Creating identity communication...')
2718 new_identity.link_comm_channel('homephone', '1234566')
2719 print('Identity communications: %s' % new_identity.get_comm_channels())
2720
2726
2728 genders, idx = get_gender_list()
2729 print("\n\nRetrieving gender enum (tag, label, weight):")
2730 for gender in genders:
2731 print("%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']]))
2732
2738
2742
2743
2747
2748
2756
2757
2761
2762
2766
2767
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791 test_mecard()
2792
2793
2794
2795
2796
2797