Coverage for crateweb/userprofile/models.py: 75%

55 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2026-02-05 06:46 -0600

1""" 

2crate_anon/crateweb/userprofile/models.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**Extended user profile for Django, with all our user configuration details.** 

27 

28""" 

29 

30from typing import Any, List, Optional, Type, TYPE_CHECKING 

31 

32from cardinal_pythonlib.django.fields.jsonclassfield import JsonClassField 

33from django.conf import settings 

34from django.db import models 

35from django.dispatch import receiver 

36from django.http.request import HttpRequest 

37 

38from crate_anon.crateweb.core.constants import ( 

39 LEN_ADDRESS, 

40 LEN_PHONE, 

41 LEN_TITLE, 

42) 

43from crate_anon.crateweb.extra.salutation import ( 

44 forename_surname, 

45 salutation, 

46 title_forename_surname, 

47) 

48 

49if TYPE_CHECKING: 

50 from crate_anon.crateweb.research.models import PatientMultiQuery 

51 

52 

53# ============================================================================= 

54# User profile information 

55# ============================================================================= 

56 

57 

58class UserProfile(models.Model): 

59 """ 

60 User profile information. 

61 

62 This is used for: 

63 

64 - stuff the user might edit, e.g. per_page 

65 - a representation of the user as a researcher (or maybe clinician) 

66 """ 

67 

68 user = models.OneToOneField( 

69 settings.AUTH_USER_MODEL, 

70 primary_key=True, 

71 on_delete=models.CASCADE, 

72 related_name="profile", 

73 ) 

74 # https://stackoverflow.com/questions/14345303/creating-a-profile-model-with-both-an-inlineadmin-and-a-post-save-signal-in-djan # noqa: E501 

75 

76 # first_name: in Django User model 

77 # last_name: in Django User model 

78 # email: in Django User model 

79 

80 N_PAGE_CHOICES = ( 

81 (10, "10"), 

82 (20, "20"), 

83 (50, "50"), 

84 (100, "100"), 

85 (200, "200"), 

86 (500, "500"), 

87 (1000, "1000"), 

88 ) 

89 N_PATIENT_PAGE_CHOICES = ( 

90 (1, "1"), 

91 (5, "5"), 

92 (10, "10"), 

93 (20, "20"), 

94 (50, "50"), 

95 (100, "100"), 

96 ) 

97 

98 # ------------------------------------------------------------------------- 

99 # Web site personal settings 

100 # ------------------------------------------------------------------------- 

101 per_page = models.PositiveSmallIntegerField( 

102 choices=N_PAGE_CHOICES, 

103 default=50, 

104 verbose_name="Number of items to show per page", 

105 ) 

106 patients_per_page = models.PositiveSmallIntegerField( 

107 choices=N_PATIENT_PAGE_CHOICES, 

108 default=1, 

109 verbose_name="Number of patients to show per page " 

110 "(for Patient Explorer view)", 

111 ) 

112 line_length = models.PositiveSmallIntegerField( 

113 default=80, 

114 verbose_name="Characters to word-wrap text at in results " 

115 "display (0 for no wrap)", 

116 ) 

117 collapse_at_len = models.PositiveSmallIntegerField( 

118 default=400, 

119 verbose_name="Number of characters beyond which results field starts " 

120 "collapsed (0 for none)", 

121 ) 

122 collapse_at_n_lines = models.PositiveSmallIntegerField( 

123 default=5, 

124 verbose_name="Number of lines beyond which result/query field starts " 

125 "collapsed (0 for none)", 

126 ) 

127 sql_scratchpad = models.TextField( 

128 verbose_name="SQL scratchpad for query builder" 

129 ) 

130 patient_multiquery_scratchpad = JsonClassField( 

131 verbose_name="PatientMultiQuery scratchpad (in JSON) for builder", 

132 null=True, 

133 ) # type: PatientMultiQuery 

134 

135 # ------------------------------------------------------------------------- 

136 # Developer 

137 # ------------------------------------------------------------------------- 

138 is_developer = models.BooleanField( 

139 default=False, verbose_name="Enable developer functions?" 

140 ) 

141 

142 # ------------------------------------------------------------------------- 

143 # Contact details 

144 # ------------------------------------------------------------------------- 

145 title = models.CharField(max_length=LEN_TITLE, blank=True) 

146 address_1 = models.CharField( 

147 max_length=LEN_ADDRESS, blank=True, verbose_name="Address line 1" 

148 ) 

149 address_2 = models.CharField( 

150 max_length=LEN_ADDRESS, blank=True, verbose_name="Address line 2" 

151 ) 

152 address_3 = models.CharField( 

153 max_length=LEN_ADDRESS, blank=True, verbose_name="Address line 3" 

154 ) 

155 address_4 = models.CharField( 

156 max_length=LEN_ADDRESS, blank=True, verbose_name="Address line 4" 

157 ) 

158 address_5 = models.CharField( 

159 max_length=LEN_ADDRESS, 

160 blank=True, 

161 verbose_name="Address line 5 (county)", 

162 ) 

163 address_6 = models.CharField( 

164 max_length=LEN_ADDRESS, 

165 blank=True, 

166 verbose_name="Address line 6 (postcode)", 

167 ) 

168 address_7 = models.CharField( 

169 max_length=LEN_ADDRESS, 

170 blank=True, 

171 verbose_name="Address line 7 (country)", 

172 ) 

173 telephone = models.CharField(max_length=LEN_PHONE, blank=True) 

174 

175 # ------------------------------------------------------------------------- 

176 # Clinician-specific bits 

177 # ------------------------------------------------------------------------- 

178 is_clinician = models.BooleanField( 

179 default=False, 

180 verbose_name="User is a clinician (with implied permission to look " 

181 "up RIDs)", 

182 ) 

183 is_consultant = models.BooleanField( 

184 default=False, 

185 verbose_name="User is an NHS consultant " 

186 "(relevant for clinical trials)", 

187 ) 

188 signatory_title = models.CharField( 

189 max_length=255, 

190 verbose_name='Title for signature (e.g. "Consultant psychiatrist")', 

191 ) 

192 

193 # ------------------------------------------------------------------------- 

194 # Functions 

195 # ------------------------------------------------------------------------- 

196 def get_address_components(self) -> List[str]: 

197 """ 

198 Returns the user's address lines. 

199 """ 

200 return list( 

201 filter( 

202 None, 

203 [ 

204 self.address_1, 

205 self.address_2, 

206 self.address_3, 

207 self.address_4, 

208 self.address_5, 

209 self.address_6, 

210 self.address_7, 

211 ], 

212 ) 

213 ) 

214 

215 def get_title_forename_surname(self) -> str: 

216 """ 

217 Returns the user's name in the form "Dr Joe Bloggs". 

218 """ 

219 # noinspection PyTypeChecker,PyUnresolvedReferences 

220 return title_forename_surname( 

221 self.title, self.user.first_name, self.user.last_name 

222 ) 

223 

224 def get_salutation(self) -> str: 

225 """ 

226 Returns a salutation for the user (e.g. "Dr Bloggs"). 

227 """ 

228 # noinspection PyTypeChecker,PyUnresolvedReferences 

229 return salutation( 

230 self.title, 

231 self.user.first_name, 

232 self.user.last_name, 

233 assume_dr=True, 

234 ) 

235 

236 def get_forename_surname(self) -> str: 

237 """ 

238 Returns the user's name in the form "Joe Bloggs". 

239 """ 

240 # noinspection PyUnresolvedReferences 

241 return forename_surname(self.user.first_name, self.user.last_name) 

242 

243 

244# noinspection PyUnusedLocal 

245@receiver(models.signals.post_save, sender=settings.AUTH_USER_MODEL) 

246def user_saved_so_create_profile( 

247 sender: Type[settings.AUTH_USER_MODEL], 

248 instance: settings.AUTH_USER_MODEL, 

249 created: bool, 

250 **kwargs: Any 

251) -> None: 

252 """ 

253 Django signal receiver. 

254 

255 Called when a Django User object has been saved. Attaches 

256 

257 Args: 

258 sender: the model class (User) 

259 instance: will be the User object 

260 created: was a new record created? 

261 **kwargs: other arguments we don't care about 

262 

263 See https://docs.djangoproject.com/en/2.1/ref/signals/#post-save. 

264 

265 """ 

266 UserProfile.objects.get_or_create(user=instance) 

267 

268 

269# ============================================================================= 

270# Helper functions 

271# ============================================================================= 

272 

273 

274def get_per_page(request: HttpRequest) -> Optional[int]: 

275 """ 

276 Returns the number of items per page (a pagination preference) of the 

277 current user. 

278 

279 Args: 

280 request: the :class:`django.http.request.HttpRequest` 

281 

282 Returns: 

283 the number of items per page, or ``None`` if the user was not 

284 authenticated 

285 

286 """ 

287 if not request.user.is_authenticated: 

288 return None 

289 # noinspection PyUnresolvedReferences 

290 profile = request.user.profile # type: UserProfile 

291 return profile.per_page 

292 

293 

294def get_patients_per_page(request: HttpRequest) -> Optional[int]: 

295 """ 

296 Returns the number of patients per page (a pagination preference, for the 

297 Patient Explorer view) of the current user. 

298 

299 Args: 

300 request: the :class:`django.http.request.HttpRequest` 

301 

302 Returns: 

303 the number of patients per page, or ``None`` if the user was not 

304 authenticated 

305 

306 """ 

307 if not request.user.is_authenticated: 

308 return None 

309 # noinspection PyUnresolvedReferences 

310 profile = request.user.profile # type: UserProfile 

311 return profile.patients_per_page