Coverage for crateweb/consent/lookup_common.py: 23%

60 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1""" 

2crate_anon/crateweb/consent/lookup_common.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CRATE. 

10 

11 CRATE is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CRATE is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CRATE. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26**Helper functions for consent-for-contact lookup processes.** 

27 

28""" 

29 

30import datetime 

31from operator import attrgetter 

32from typing import List, Optional, Union 

33 

34from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist 

35 

36from crate_anon.crateweb.consent.models import ( 

37 ClinicianInfoHolder, 

38 PatientLookup, 

39 TeamRep, 

40) 

41from crate_anon.crateweb.consent.utils import latest_date 

42 

43 

44# ============================================================================= 

45# Constants 

46# ============================================================================= 

47 

48 

49class SignatoryTitles: 

50 """ 

51 Standard signatory titles for clinicians of various kinds. 

52 """ 

53 

54 CARE_COORDINATOR = "Care coordinator" 

55 CLINICIAN = "Clinician" 

56 CONS_PSYCHIATRIST = "Consultant psychiatrist" 

57 CONSULTANT = "Consultant" 

58 TEAM_MEMBER = "Clinical team member" 

59 

60 

61# ============================================================================= 

62# Create a clinician object to represent a team, ideally personalized to that 

63# team's known representative. 

64# ============================================================================= 

65 

66 

67def get_team_details( 

68 team_name: str, 

69 start_date: Union[datetime.date, datetime.datetime], 

70 end_date: Optional[Union[datetime.date, datetime.datetime]], 

71 decisions: List[str], 

72) -> ClinicianInfoHolder: 

73 """ 

74 Modify ``team_info`` if possible to add a team representative's details. 

75 

76 Args: 

77 team_name: 

78 Name of the team to look up. 

79 start_date: 

80 Start date for the team's involvement. 

81 end_date: 

82 Optional end date for the team's involvement. 

83 decisions: 

84 Log of decisions made. Will be written to. 

85 """ 

86 team_info = ClinicianInfoHolder( 

87 clinician_type=ClinicianInfoHolder.TEAM, 

88 title="", 

89 first_name="", 

90 surname="", 

91 email="", 

92 signatory_title=SignatoryTitles.TEAM_MEMBER, 

93 is_consultant=False, 

94 start_date=start_date, 

95 end_date=end_date, 

96 ) 

97 # We know a team - do we have a team representative? 

98 team_summary = "{status} team {desc}".format( 

99 status="active" if team_info.end_date is None else "previous", 

100 desc=repr(team_name), 

101 ) 

102 try: 

103 teamrep = TeamRep.objects.get(team=team_name) 

104 decisions.append("Clinical team representative found.") 

105 profile = teamrep.user.profile 

106 team_info.title = profile.title 

107 team_info.first_name = teamrep.user.first_name 

108 team_info.surname = teamrep.user.last_name 

109 team_info.email = teamrep.user.email 

110 team_info.signatory_title = profile.signatory_title 

111 team_info.is_consultant = profile.is_consultant 

112 except ObjectDoesNotExist: 

113 decisions.append(f"No team representative found for {team_summary}.") 

114 except MultipleObjectsReturned: 

115 decisions.append( 

116 f"Confused: >1 team representative found for {team_summary}." 

117 ) 

118 return team_info 

119 # We return it even if we can't find a representative, because it still 

120 # carries information about whether the patient is discharged or not. 

121 

122 

123# ============================================================================= 

124# Pick the most appropriate of several possible clinicians 

125# ============================================================================= 

126 

127 

128def pick_best_clinician( 

129 lookup: PatientLookup, 

130 clinicians: List[ClinicianInfoHolder], 

131 decisions: List[str], 

132) -> None: 

133 """ 

134 By now we know all relevant recent clinicians, including (potentially) ones 

135 from which the patient has been discharged, and ones that are active. 

136 

137 Work through possible clinicians and see who's the best to pick (e.g. is 

138 contactable!). Store that information back in the lookup, 

139 

140 Args: 

141 lookup: 

142 Patient being looked up. Will be modified. 

143 clinicians: 

144 Candidate clinicians. 

145 decisions: 

146 Log of decisions made. Will be written to. 

147 """ 

148 decisions.append( 

149 f"{len(clinicians)} total past/present " 

150 f"clinician(s)/team(s) found: {clinicians!r}." 

151 ) 

152 current_clinicians = [c for c in clinicians if c.current()] 

153 if current_clinicians: 

154 lookup.pt_discharged = False 

155 lookup.pt_discharge_date = None 

156 decisions.append("Patient not discharged.") 

157 contactable_curr_clin = [ 

158 c for c in current_clinicians if c.contactable() 

159 ] 

160 # Sorting by two keys: https://stackoverflow.com/questions/11206884 

161 # LOW priority: most recent clinician. (Goes first in sort.) 

162 # HIGH priority: preferred type of clinician. (Goes last in sort.) 

163 # Sort order is: most preferred first. 

164 contactable_curr_clin.sort(key=attrgetter("start_date"), reverse=True) 

165 contactable_curr_clin.sort( 

166 key=attrgetter("clinician_preference_order") 

167 ) 

168 decisions.append( 

169 f"{len(contactable_curr_clin)} contactable active " 

170 f"clinician(s) found." 

171 ) 

172 if contactable_curr_clin: 

173 chosen_clinician = contactable_curr_clin[0] 

174 lookup.set_from_clinician_info_holder(chosen_clinician) 

175 decisions.append( 

176 f"Found active clinician of type: " 

177 f"{chosen_clinician.clinician_type}" 

178 ) 

179 return # All done! 

180 # If we get here, the patient is not discharged, but we haven't found 

181 # a contactable active clinician. 

182 # We'll fall through and check older clinicians for contactability. 

183 else: 

184 end_dates = [c.end_date for c in clinicians] 

185 lookup.pt_discharged = True 

186 lookup.pt_discharge_date = latest_date(*end_dates) 

187 decisions.append("Patient discharged.") 

188 

189 # We get here either if the patient is discharged, or they're current but 

190 # we can't contact a current clinician. 

191 contactable_old_clin = [c for c in clinicians if c.contactable()] 

192 # LOW priority: preferred type of clinician. (Goes first in sort.) 

193 # HIGH priority: most recent end date. (Goes last in sort.) 

194 # Sort order is: most preferred first. 

195 contactable_old_clin.sort(key=attrgetter("clinician_preference_order")) 

196 contactable_old_clin.sort(key=attrgetter("end_date"), reverse=True) 

197 decisions.append( 

198 f"{len(contactable_old_clin)} contactable previous " 

199 f"clinician(s) found." 

200 ) 

201 if contactable_old_clin: 

202 chosen_clinician = contactable_old_clin[0] 

203 lookup.set_from_clinician_info_holder(chosen_clinician) 

204 decisions.append( 

205 f"Found previous clinician of type: " 

206 f"{chosen_clinician.clinician_type}" 

207 ) 

208 

209 if not lookup.clinician_found: 

210 decisions.append("Failed to establish contactable clinician.")