Coverage for cc_modules/cc_testfactories.py: 91%
245 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:51 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:51 +0100
1"""
2camcops_server/cc_modules/cc_testfactories.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS 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 CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26**Factory Boy SQL Alchemy test factories.**
28"""
30from typing import Any, cast, Optional, TYPE_CHECKING
32from cardinal_pythonlib.datetimefunc import (
33 convert_datetime_to_utc,
34 format_datetime,
35)
36import factory
37from faker import Faker
38import pendulum
40from camcops_server.cc_modules.cc_blob import Blob
41from camcops_server.cc_modules.cc_constants import DateFormat, ERA_NOW
42from camcops_server.cc_modules.cc_device import Device
43from camcops_server.cc_modules.cc_dirtytables import DirtyTable
44from camcops_server.cc_modules.cc_email import Email
45from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient
46from camcops_server.cc_modules.cc_group import Group
47from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition
48from camcops_server.cc_modules.cc_ipuse import IpUse
49from camcops_server.cc_modules.cc_membership import UserGroupMembership
50from camcops_server.cc_modules.cc_patient import Patient
51from camcops_server.cc_modules.cc_patientidnum import PatientIdNum
52from camcops_server.cc_modules.cc_specialnote import SpecialNote
53from camcops_server.cc_modules.cc_testproviders import register_all_providers
54from camcops_server.cc_modules.cc_taskschedule import (
55 PatientTaskSchedule,
56 PatientTaskScheduleEmail,
57 TaskSchedule,
58 TaskScheduleItem,
59)
60from camcops_server.cc_modules.cc_user import User
62if TYPE_CHECKING:
63 from factory.builder import Resolver
64 from camcops_server.cc_modules.cc_request import CamcopsRequest
67# Avoid any ID clashes with objects not created with factories
68ID_OFFSET = 1000
69RIO_ID_OFFSET = 10000
70STUDY_ID_OFFSET = 5000
73class Fake:
74 # Factory Boy has its own interface to Faker (factory.Faker()). This
75 # takes a function to be called at object generation time and as far as I
76 # can tell this doesn't support being able to create fake data based on
77 # other fake attributes such as notes for a patient. You can work
78 # around this by adding a lot of logic to the factories. To me it makes
79 # sense to keep the factories simple and do as much as possible of the
80 # content generation in the providers. So we call Faker directly instead.
81 en_gb = Faker("en_GB") # For UK postcodes, phone numbers etc
82 en_us = Faker("en_US") # en_GB gives Lorem ipsum for pad words.
85register_all_providers(Fake.en_gb)
88# sqlalchemy_session gets poked in by DemoRequestCase.setUp()
89class BaseFactory(factory.alchemy.SQLAlchemyModelFactory):
90 class Meta:
91 sqlalchemy_session_persistence = "commit"
94class DeviceFactory(BaseFactory):
95 class Meta:
96 model = Device
98 id = factory.Sequence(lambda n: n + ID_OFFSET)
99 name = factory.Sequence(lambda n: f"test-device-{n + ID_OFFSET}")
102class IpUseFactory(BaseFactory):
103 class Meta:
104 model = IpUse
106 clinical = factory.LazyFunction(Fake.en_gb.pybool)
107 commercial = factory.LazyFunction(Fake.en_gb.pybool)
108 educational = factory.LazyFunction(Fake.en_gb.pybool)
109 research = factory.LazyFunction(Fake.en_gb.pybool)
112class GroupFactory(BaseFactory):
113 class Meta:
114 model = Group
116 id = factory.Sequence(lambda n: n + ID_OFFSET)
117 name = factory.Sequence(lambda n: f"Group {n + ID_OFFSET}")
118 ip_use = factory.SubFactory(IpUseFactory)
121class AnyIdNumGroupFactory(GroupFactory):
122 upload_policy = "sex and anyidnum"
123 finalize_policy = "sex and anyidnum"
126class UserFactory(BaseFactory):
127 class Meta:
128 model = User
130 username = factory.Sequence(lambda n: f"user{n}")
131 hashedpw = ""
133 @factory.post_generation
134 def password(
135 obj: User,
136 create: bool,
137 password: Optional[str],
138 request: "CamcopsRequest" = None,
139 **kwargs: Any,
140 ) -> None:
141 if not create:
142 return
144 if password is None:
145 return
147 assert request is not None
149 obj.set_password(request, password)
152class GenericTabletRecordFactory(BaseFactory):
153 class Meta:
154 exclude = ("default_iso_datetime",)
155 abstract = True
157 default_iso_datetime = "1970-01-01T12:00"
159 _pk = factory.Sequence(lambda n: n + ID_OFFSET)
160 _device = factory.SubFactory(DeviceFactory)
161 _group = factory.SubFactory(AnyIdNumGroupFactory)
162 _adding_user = factory.SubFactory(UserFactory)
164 @factory.lazy_attribute
165 def _when_added_exact(obj: "Resolver") -> pendulum.DateTime:
166 datetime = cast(
167 pendulum.DateTime, pendulum.parse(obj.default_iso_datetime)
168 )
170 return datetime
172 @factory.lazy_attribute
173 def _when_added_batch_utc(obj: "Resolver") -> pendulum.DateTime:
174 era_time = pendulum.parse(obj.default_iso_datetime)
175 return convert_datetime_to_utc(era_time) # type: ignore[arg-type]
177 @factory.lazy_attribute
178 def _era(obj: "Resolver") -> str:
179 era_time = pendulum.parse(obj.default_iso_datetime)
180 return format_datetime(era_time, DateFormat.ISO8601) # type: ignore[arg-type] # noqa: E501
182 @factory.lazy_attribute
183 def _current(obj: "Resolver") -> bool:
184 # _current = True gets ignored for some reason
185 return True
187 @factory.lazy_attribute
188 def when_last_modified(obj: "Resolver") -> str:
189 era_time = pendulum.parse(obj.default_iso_datetime)
190 return format_datetime(era_time, DateFormat.ISO8601) # type: ignore[arg-type] # noqa: E501
193class PatientFactory(GenericTabletRecordFactory):
194 class Meta:
195 model = Patient
197 id = factory.Sequence(lambda n: n + ID_OFFSET)
198 sex = factory.LazyFunction(Fake.en_gb.sex)
199 dob = factory.LazyFunction(Fake.en_gb.consistent_date_of_birth)
200 address = factory.LazyFunction(Fake.en_gb.address)
201 gp = factory.LazyFunction(Fake.en_gb.name)
202 other = factory.LazyFunction(Fake.en_us.paragraph)
203 email = factory.LazyFunction(Fake.en_gb.email)
205 @factory.lazy_attribute
206 def forename(obj: "Resolver") -> str:
207 return Fake.en_gb.forename(obj.sex)
209 surname = factory.LazyFunction(Fake.en_gb.last_name)
212class ServerCreatedPatientFactory(PatientFactory):
213 @factory.lazy_attribute
214 def _device(obj: "Resolver") -> Device:
215 # May have been created in BasicDatabaseTestCase.setUp
216 return Device.get_server_device(
217 ServerCreatedPatientFactory._meta.sqlalchemy_session
218 )
220 @factory.lazy_attribute
221 def _era(obj: "Resolver") -> str:
222 return ERA_NOW
225class IdNumDefinitionFactory(BaseFactory):
226 class Meta:
227 model = IdNumDefinition
229 which_idnum = factory.Sequence(lambda n: n + ID_OFFSET)
232class NHSIdNumDefinitionFactory(IdNumDefinitionFactory):
233 description = "NHS number"
234 short_description = "NHS#"
235 hl7_assigning_authority = "NHS"
236 hl7_id_type = "NHSN"
237 validation_method = "uk_nhs_number"
240class StudyIdNumDefinitionFactory(IdNumDefinitionFactory):
241 description = "Study number"
242 short_description = "Study"
245class RioIdNumDefinitionFactory(IdNumDefinitionFactory):
246 description = "RiO number"
247 short_description = "RiO"
248 hl7_assigning_authority = "CPFT"
249 hl7_id_type = "CPRiO"
252class PatientIdNumFactory(GenericTabletRecordFactory):
253 class Meta:
254 model = PatientIdNum
256 id = factory.Sequence(lambda n: n + ID_OFFSET)
257 patient = factory.SubFactory(PatientFactory)
258 patient_id = factory.SelfAttribute("patient.id")
259 _group = factory.SelfAttribute("patient._group")
260 _device = factory.SelfAttribute("patient._device")
263class NHSPatientIdNumFactory(PatientIdNumFactory):
264 class Meta:
265 exclude = PatientIdNumFactory._meta.exclude + ("iddef",)
267 iddef = factory.SubFactory(NHSIdNumDefinitionFactory)
269 which_idnum = factory.SelfAttribute("iddef.which_idnum")
270 idnum_value = factory.LazyFunction(Fake.en_gb.nhs_number)
273class RioPatientIdNumFactory(PatientIdNumFactory):
274 class Meta:
275 exclude = PatientIdNumFactory._meta.exclude + ("iddef",)
277 iddef = factory.SubFactory(RioIdNumDefinitionFactory)
279 which_idnum = factory.SelfAttribute("iddef.which_idnum")
280 idnum_value = factory.Sequence(lambda n: n + RIO_ID_OFFSET)
283class StudyPatientIdNumFactory(PatientIdNumFactory):
284 class Meta:
285 exclude = PatientIdNumFactory._meta.exclude + ("iddef",)
287 iddef = factory.SubFactory(StudyIdNumDefinitionFactory)
289 which_idnum = factory.SelfAttribute("iddef.which_idnum")
290 idnum_value = factory.Sequence(lambda n: n + STUDY_ID_OFFSET)
293class ServerCreatedPatientIdNumFactory(PatientIdNumFactory):
294 patient = factory.SubFactory(ServerCreatedPatientFactory)
296 @factory.lazy_attribute
297 def _device(obj: "Resolver") -> Device:
298 # Should have been created in BasicDatabaseTestCase.setUp
299 return Device.get_server_device(
300 ServerCreatedPatientIdNumFactory._meta.sqlalchemy_session
301 )
303 @factory.lazy_attribute
304 def _era(obj: "Resolver") -> str:
305 return ERA_NOW
308class ServerCreatedNHSPatientIdNumFactory(
309 ServerCreatedPatientIdNumFactory, NHSPatientIdNumFactory
310):
311 class Meta:
312 exclude = (
313 ServerCreatedPatientIdNumFactory._meta.exclude
314 + NHSPatientIdNumFactory._meta.exclude
315 )
318class ServerCreatedRioPatientIdNumFactory(
319 ServerCreatedPatientIdNumFactory, RioPatientIdNumFactory
320):
321 class Meta:
322 exclude = (
323 ServerCreatedPatientIdNumFactory._meta.exclude
324 + RioPatientIdNumFactory._meta.exclude
325 )
328class ServerCreatedStudyPatientIdNumFactory(
329 ServerCreatedPatientIdNumFactory, StudyPatientIdNumFactory
330):
331 class Meta:
332 exclude = (
333 ServerCreatedPatientIdNumFactory._meta.exclude
334 + StudyPatientIdNumFactory._meta.exclude
335 )
338class TaskScheduleFactory(BaseFactory):
339 class Meta:
340 model = TaskSchedule
342 group = factory.SubFactory(GroupFactory)
343 name = factory.Sequence(lambda n: f"Schedule {n + ID_OFFSET}")
346class TaskScheduleItemFactory(BaseFactory):
347 class Meta:
348 model = TaskScheduleItem
350 task_schedule = factory.SubFactory(TaskScheduleFactory)
353class PatientTaskScheduleFactory(BaseFactory):
354 class Meta:
355 model = PatientTaskSchedule
357 task_schedule = factory.SubFactory(TaskScheduleFactory)
358 # If patient has not been set explicitly,
359 # ensure Patient and TaskSchedule end up in the same group
360 start_datetime = None
361 patient = factory.SubFactory(
362 ServerCreatedPatientFactory,
363 _group=factory.SelfAttribute("..task_schedule.group"),
364 )
367class EmailFactory(BaseFactory):
368 class Meta:
369 model = Email
371 # Although sent and sent_at_utc are columns, they are not keyword
372 # arguments to Email's constructor so they are populated after the object
373 # has been created. For some reason 'sent' needs to be set explicitly
374 # when creating the factory even though the default should be False. Might
375 # be a SQLite thing.
376 @factory.post_generation
377 def sent_at_utc(
378 obj: Email, create: bool, sent_at_utc: pendulum.DateTime, **kwargs: Any
379 ) -> None:
380 if not create:
381 return
383 obj.sent_at_utc = sent_at_utc
385 @factory.post_generation
386 def sent(obj: Email, create: bool, sent: bool, **kwargs: Any) -> None:
387 if not create:
388 return
390 obj.sent = sent
393class PatientTaskScheduleEmailFactory(BaseFactory):
394 class Meta:
395 model = PatientTaskScheduleEmail
397 patient_task_schedule = factory.SubFactory(
398 PatientTaskScheduleFactory,
399 )
400 email = factory.SubFactory(EmailFactory, sent=True)
403class UserGroupMembershipFactory(BaseFactory):
404 class Meta:
405 model = UserGroupMembership
408class BlobFactory(GenericTabletRecordFactory):
409 class Meta:
410 model = Blob
412 id = factory.Sequence(lambda n: n + ID_OFFSET)
415class DirtyTableFactory(BaseFactory):
416 class Meta:
417 model = DirtyTable
420class SpecialNoteFactory(BaseFactory):
421 class Meta:
422 model = SpecialNote
424 @classmethod
425 def create(cls, *args: Any, **kwargs: Any) -> SpecialNote:
426 task = kwargs.pop("task", None)
427 if task is not None:
428 if "task_id" in kwargs:
429 raise TypeError(
430 "Both 'task' and 'task_id' keyword arguments "
431 f"unexpectedly passed to {cls.__name__}. Use one or the "
432 "other."
433 )
434 kwargs["task_id"] = task.id
436 if "basetable" not in kwargs:
437 kwargs["basetable"] = task.__tablename__
438 if "device_id" not in kwargs:
439 kwargs["device_id"] = task._device.id
440 if "era" not in kwargs:
441 kwargs["era"] = task._era
443 return super().create(*args, **kwargs)
446class ExportRecipientFactory(BaseFactory):
447 class Meta:
448 exclude = ("iddef",)
449 model = ExportRecipient
451 id = factory.Sequence(lambda n: n + ID_OFFSET)
453 iddef = factory.SubFactory(IdNumDefinitionFactory)
454 primary_idnum = factory.SelfAttribute("iddef.which_idnum")