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
« prev ^ index » next coverage.py v7.8.0, created at 2026-02-05 06:46 -0600
1"""
2crate_anon/crateweb/userprofile/models.py
4===============================================================================
6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CRATE.
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.
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.
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/>.
24===============================================================================
26**Extended user profile for Django, with all our user configuration details.**
28"""
30from typing import Any, List, Optional, Type, TYPE_CHECKING
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
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)
49if TYPE_CHECKING:
50 from crate_anon.crateweb.research.models import PatientMultiQuery
53# =============================================================================
54# User profile information
55# =============================================================================
58class UserProfile(models.Model):
59 """
60 User profile information.
62 This is used for:
64 - stuff the user might edit, e.g. per_page
65 - a representation of the user as a researcher (or maybe clinician)
66 """
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
76 # first_name: in Django User model
77 # last_name: in Django User model
78 # email: in Django User model
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 )
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
135 # -------------------------------------------------------------------------
136 # Developer
137 # -------------------------------------------------------------------------
138 is_developer = models.BooleanField(
139 default=False, verbose_name="Enable developer functions?"
140 )
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)
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 )
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 )
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 )
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 )
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)
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.
255 Called when a Django User object has been saved. Attaches
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
263 See https://docs.djangoproject.com/en/2.1/ref/signals/#post-save.
265 """
266 UserProfile.objects.get_or_create(user=instance)
269# =============================================================================
270# Helper functions
271# =============================================================================
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.
279 Args:
280 request: the :class:`django.http.request.HttpRequest`
282 Returns:
283 the number of items per page, or ``None`` if the user was not
284 authenticated
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
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.
299 Args:
300 request: the :class:`django.http.request.HttpRequest`
302 Returns:
303 the number of patients per page, or ``None`` if the user was not
304 authenticated
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