Coverage for tasks/icd10manic.py: 37%
175 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/icd10manic.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28"""
30from typing import List, Optional
32from cardinal_pythonlib.datetimefunc import format_datetime
33from cardinal_pythonlib.typetests import is_false
34import cardinal_pythonlib.rnc_web as ws
35from sqlalchemy.sql.schema import Column
36from sqlalchemy.sql.sqltypes import Boolean, Date, UnicodeText
38from camcops_server.cc_modules.cc_constants import (
39 CssClass,
40 DateFormat,
41 ICD10_COPYRIGHT_DIV,
42)
43from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
44from camcops_server.cc_modules.cc_html import (
45 get_present_absent_none,
46 heading_spanning_two_columns,
47 subheading_spanning_two_columns,
48 tr_qa,
49)
50from camcops_server.cc_modules.cc_request import CamcopsRequest
51from camcops_server.cc_modules.cc_sqla_coltypes import (
52 BIT_CHECKER,
53 CamcopsColumn,
54 SummaryCategoryColType,
55)
56from camcops_server.cc_modules.cc_string import AS
57from camcops_server.cc_modules.cc_summaryelement import SummaryElement
58from camcops_server.cc_modules.cc_task import (
59 Task,
60 TaskHasClinicianMixin,
61 TaskHasPatientMixin,
62)
63from camcops_server.cc_modules.cc_text import SS
66# =============================================================================
67# Icd10Manic
68# =============================================================================
71class Icd10Manic(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
72 """
73 Server implementation of the ICD10-MANIC task.
74 """
76 __tablename__ = "icd10manic"
77 shortname = "ICD10-MANIC"
78 info_filename_stem = "icd"
80 mood_elevated = CamcopsColumn(
81 "mood_elevated",
82 Boolean,
83 permitted_value_checker=BIT_CHECKER,
84 comment="The mood is 'elevated' [hypomania] or 'predominantly "
85 "elevated [or] expansive' [mania] to a degree that is "
86 "definitely abnormal for the individual concerned.",
87 )
88 mood_irritable = CamcopsColumn(
89 "mood_irritable",
90 Boolean,
91 permitted_value_checker=BIT_CHECKER,
92 comment="The mood is 'irritable' [hypomania] or 'predominantly "
93 "irritable' [mania] to a degree that is definitely abnormal "
94 "for the individual concerned.",
95 )
97 distractible = CamcopsColumn(
98 "distractible",
99 Boolean,
100 permitted_value_checker=BIT_CHECKER,
101 comment="Difficulty in concentration or distractibility [from "
102 "the criteria for hypomania]; distractibility or constant "
103 "changes in activity or plans [from the criteria for mania].",
104 )
105 activity = CamcopsColumn(
106 "activity",
107 Boolean,
108 permitted_value_checker=BIT_CHECKER,
109 comment="Increased activity or physical restlessness.",
110 )
111 sleep = CamcopsColumn(
112 "sleep",
113 Boolean,
114 permitted_value_checker=BIT_CHECKER,
115 comment="Decreased need for sleep.",
116 )
117 talkativeness = CamcopsColumn(
118 "talkativeness",
119 Boolean,
120 permitted_value_checker=BIT_CHECKER,
121 comment="Increased talkativeness (pressure of speech).",
122 )
123 recklessness = CamcopsColumn(
124 "recklessness",
125 Boolean,
126 permitted_value_checker=BIT_CHECKER,
127 comment="Mild spending sprees, or other types of reckless or "
128 "irresponsible behaviour [hypomania]; behaviour which is "
129 "foolhardy or reckless and whose risks the subject does not "
130 "recognize e.g. spending sprees, foolish enterprises, "
131 "reckless driving [mania].",
132 )
133 social_disinhibition = CamcopsColumn(
134 "social_disinhibition",
135 Boolean,
136 permitted_value_checker=BIT_CHECKER,
137 comment="Increased sociability or over-familiarity [hypomania]; "
138 "loss of normal social inhibitions resulting in behaviour "
139 "which is inappropriate to the circumstances [mania].",
140 )
141 sexual = CamcopsColumn(
142 "sexual",
143 Boolean,
144 permitted_value_checker=BIT_CHECKER,
145 comment="Increased sexual energy [hypomania]; marked sexual "
146 "energy or sexual indiscretions [mania].",
147 )
149 grandiosity = CamcopsColumn(
150 "grandiosity",
151 Boolean,
152 permitted_value_checker=BIT_CHECKER,
153 comment="Inflated self-esteem or grandiosity.",
154 )
155 flight_of_ideas = CamcopsColumn(
156 "flight_of_ideas",
157 Boolean,
158 permitted_value_checker=BIT_CHECKER,
159 comment="Flight of ideas or the subjective experience of "
160 "thoughts racing.",
161 )
163 sustained4days = CamcopsColumn(
164 "sustained4days",
165 Boolean,
166 permitted_value_checker=BIT_CHECKER,
167 comment="Elevated/irritable mood sustained for at least 4 days.",
168 )
169 sustained7days = CamcopsColumn(
170 "sustained7days",
171 Boolean,
172 permitted_value_checker=BIT_CHECKER,
173 comment="Elevated/irritable mood sustained for at least 7 days.",
174 )
175 admission_required = CamcopsColumn(
176 "admission_required",
177 Boolean,
178 permitted_value_checker=BIT_CHECKER,
179 comment="Elevated/irritable mood severe enough to require "
180 "hospital admission.",
181 )
182 some_interference_functioning = CamcopsColumn(
183 "some_interference_functioning",
184 Boolean,
185 permitted_value_checker=BIT_CHECKER,
186 comment="Some interference with personal functioning "
187 "in daily living.",
188 )
189 severe_interference_functioning = CamcopsColumn(
190 "severe_interference_functioning",
191 Boolean,
192 permitted_value_checker=BIT_CHECKER,
193 comment="Severe interference with personal "
194 "functioning in daily living.",
195 )
197 perceptual_alterations = CamcopsColumn(
198 "perceptual_alterations",
199 Boolean,
200 permitted_value_checker=BIT_CHECKER,
201 comment="Perceptual alterations (e.g. subjective hyperacusis, "
202 "appreciation of colours as specially vivid, etc.).",
203 ) # ... not psychotic
204 hallucinations_schizophrenic = CamcopsColumn(
205 "hallucinations_schizophrenic",
206 Boolean,
207 permitted_value_checker=BIT_CHECKER,
208 comment="Hallucinations that are 'typically schizophrenic' "
209 "(hallucinatory voices giving a running commentary on the "
210 "patient's behaviour, or discussing him between themselves, "
211 "or other types of hallucinatory voices coming from some part "
212 "of the body).",
213 )
214 hallucinations_other = CamcopsColumn(
215 "hallucinations_other",
216 Boolean,
217 permitted_value_checker=BIT_CHECKER,
218 comment="Hallucinations (of any other kind).",
219 )
220 delusions_schizophrenic = CamcopsColumn(
221 "delusions_schizophrenic",
222 Boolean,
223 permitted_value_checker=BIT_CHECKER,
224 comment="Delusions that are 'typically schizophrenic' (delusions "
225 "of control, influence or passivity, clearly referred to body "
226 "or limb movements or specific thoughts, actions, or "
227 "sensations; delusional perception; persistent delusions of "
228 "other kinds that are culturally inappropriate and completely "
229 "impossible).",
230 )
231 delusions_other = CamcopsColumn(
232 "delusions_other",
233 Boolean,
234 permitted_value_checker=BIT_CHECKER,
235 comment="Delusions (of any other kind).",
236 )
238 date_pertains_to = Column(
239 "date_pertains_to", Date, comment="Date the assessment pertains to"
240 )
241 comments = Column("comments", UnicodeText, comment="Clinician's comments")
243 CORE_NAMES = ["mood_elevated", "mood_irritable"]
244 HYPOMANIA_MANIA_NAMES = [
245 "distractible",
246 "activity",
247 "sleep",
248 "talkativeness",
249 "recklessness",
250 "social_disinhibition",
251 "sexual",
252 ]
253 MANIA_NAMES = ["grandiosity", "flight_of_ideas"]
254 OTHER_CRITERIA_NAMES = [
255 "sustained4days",
256 "sustained7days",
257 "admission_required",
258 "some_interference_functioning",
259 "severe_interference_functioning",
260 ]
261 PSYCHOSIS_NAMES = [
262 "perceptual_alterations", # not psychotic
263 "hallucinations_schizophrenic",
264 "hallucinations_other",
265 "delusions_schizophrenic",
266 "delusions_other",
267 ]
269 @staticmethod
270 def longname(req: "CamcopsRequest") -> str:
271 _ = req.gettext
272 return _(
273 "ICD-10 symptomatic criteria for a manic/hypomanic episode "
274 "(as in e.g. F06.3, F25, F30, F31)"
275 )
277 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
278 if not self.is_complete():
279 return CTV_INCOMPLETE
280 infolist = [
281 CtvInfo(
282 content="Pertains to: {}. Category: {}.".format(
283 format_datetime(
284 self.date_pertains_to, DateFormat.LONG_DATE
285 ),
286 self.get_description(req),
287 )
288 )
289 ]
290 if self.comments:
291 infolist.append(CtvInfo(content=ws.webify(self.comments)))
292 return infolist
294 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
295 return self.standard_task_summary_fields() + [
296 SummaryElement(
297 name="category",
298 coltype=SummaryCategoryColType,
299 value=self.get_description(req),
300 comment="Diagnostic category",
301 ),
302 SummaryElement(
303 name="psychotic_symptoms",
304 coltype=Boolean(),
305 value=self.psychosis_present(),
306 comment="Psychotic symptoms present?",
307 ),
308 ]
310 # Meets criteria? These also return null for unknown.
311 def meets_criteria_mania_psychotic_schizophrenic(self) -> Optional[bool]:
312 x = self.meets_criteria_mania_ignoring_psychosis()
313 if not x:
314 return x
315 if self.hallucinations_other or self.delusions_other:
316 return False # that counts as manic psychosis
317 if self.hallucinations_other is None or self.delusions_other is None:
318 return None # might be manic psychosis
319 if self.hallucinations_schizophrenic or self.delusions_schizophrenic:
320 return True
321 if (
322 self.hallucinations_schizophrenic is None
323 or self.delusions_schizophrenic is None
324 ):
325 return None
326 return False
328 def meets_criteria_mania_psychotic_icd(self) -> Optional[bool]:
329 x = self.meets_criteria_mania_ignoring_psychosis()
330 if not x:
331 return x
332 if self.hallucinations_other or self.delusions_other:
333 return True
334 if self.hallucinations_other is None or self.delusions_other is None:
335 return None
336 return False
338 def meets_criteria_mania_nonpsychotic(self) -> Optional[bool]:
339 x = self.meets_criteria_mania_ignoring_psychosis()
340 if not x:
341 return x
342 if (
343 self.hallucinations_schizophrenic is None
344 or self.delusions_schizophrenic is None
345 or self.hallucinations_other is None
346 or self.delusions_other is None
347 ):
348 return None
349 if (
350 self.hallucinations_schizophrenic
351 or self.delusions_schizophrenic
352 or self.hallucinations_other
353 or self.delusions_other
354 ):
355 return False
356 return True
358 def meets_criteria_mania_ignoring_psychosis(self) -> Optional[bool]:
359 # When can we say "definitely not"?
360 if is_false(self.mood_elevated) and is_false(self.mood_irritable):
361 return False
362 if is_false(self.sustained7days) and is_false(self.admission_required):
363 return False
364 t = self.count_booleans(
365 self.HYPOMANIA_MANIA_NAMES
366 ) + self.count_booleans(self.MANIA_NAMES)
367 u = self.n_fields_none(
368 self.HYPOMANIA_MANIA_NAMES
369 ) + self.n_fields_none(self.MANIA_NAMES)
370 if self.mood_elevated and (t + u < 3):
371 # With elevated mood, need at least 3 symptoms
372 return False
373 if is_false(self.mood_elevated) and (t + u < 4):
374 # With only irritable mood, need at least 4 symptoms
375 return False
376 if is_false(self.severe_interference_functioning):
377 return False
378 # OK. When can we say "yes"?
379 if (
380 (self.mood_elevated or self.mood_irritable)
381 and (self.sustained7days or self.admission_required)
382 and (
383 (self.mood_elevated and t >= 3)
384 or (self.mood_irritable and t >= 4)
385 )
386 and self.severe_interference_functioning
387 ):
388 return True
389 return None
391 def meets_criteria_hypomania(self) -> Optional[bool]:
392 # When can we say "definitely not"?
393 if self.meets_criteria_mania_ignoring_psychosis():
394 return False # silly to call it hypomania if it's mania
395 if is_false(self.mood_elevated) and is_false(self.mood_irritable):
396 return False
397 if is_false(self.sustained4days):
398 return False
399 t = self.count_booleans(self.HYPOMANIA_MANIA_NAMES)
400 u = self.n_fields_none(self.HYPOMANIA_MANIA_NAMES)
401 if t + u < 3:
402 # Need at least 3 symptoms
403 return False
404 if is_false(self.some_interference_functioning):
405 return False
406 # OK. When can we say "yes"?
407 if (
408 (self.mood_elevated or self.mood_irritable)
409 and self.sustained4days
410 and t >= 3
411 and self.some_interference_functioning
412 ):
413 return True
414 return None
416 def meets_criteria_none(self) -> Optional[bool]:
417 h = self.meets_criteria_hypomania()
418 m = self.meets_criteria_mania_ignoring_psychosis()
419 if h or m:
420 return False
421 if is_false(h) and is_false(m):
422 return True
423 return None
425 def psychosis_present(self) -> Optional[bool]:
426 if (
427 self.hallucinations_other
428 or self.hallucinations_schizophrenic
429 or self.delusions_other
430 or self.delusions_schizophrenic
431 ):
432 return True
433 if (
434 self.hallucinations_other is None
435 or self.hallucinations_schizophrenic is None
436 or self.delusions_other is None
437 or self.delusions_schizophrenic is None
438 ):
439 return None
440 return False
442 def get_description(self, req: CamcopsRequest) -> str:
443 if self.meets_criteria_mania_psychotic_schizophrenic():
444 return self.wxstring(req, "category_manic_psychotic_schizophrenic")
445 elif self.meets_criteria_mania_psychotic_icd():
446 return self.wxstring(req, "category_manic_psychotic")
447 elif self.meets_criteria_mania_nonpsychotic():
448 return self.wxstring(req, "category_manic_nonpsychotic")
449 elif self.meets_criteria_hypomania():
450 return self.wxstring(req, "category_hypomanic")
451 elif self.meets_criteria_none():
452 return self.wxstring(req, "category_none")
453 else:
454 return req.sstring(SS.UNKNOWN)
456 def is_complete(self) -> bool:
457 return (
458 self.date_pertains_to is not None
459 and self.meets_criteria_none() is not None
460 and self.field_contents_valid()
461 )
463 def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
464 return heading_spanning_two_columns(self.wxstring(req, wstringname))
466 def row_true_false(self, req: CamcopsRequest, fieldname: str) -> str:
467 return self.get_twocol_bool_row_true_false(
468 req, fieldname, self.wxstring(req, "" + fieldname)
469 )
471 def get_task_html(self, req: CamcopsRequest) -> str:
472 h = """
473 {clinician_comments}
474 <div class="{CssClass.SUMMARY}">
475 <table class="{CssClass.SUMMARY}">
476 {tr_is_complete}
477 {date_pertains_to}
478 {category}
479 {psychotic_symptoms}
480 </table>
481 </div>
482 <div class="{CssClass.EXPLANATION}">
483 {icd10_symptomatic_disclaimer}
484 </div>
485 <table class="{CssClass.TASKDETAIL}">
486 <tr>
487 <th width="80%">Question</th>
488 <th width="20%">Answer</th>
489 </tr>
490 """.format(
491 clinician_comments=self.get_standard_clinician_comments_block(
492 req, self.comments
493 ),
494 CssClass=CssClass,
495 tr_is_complete=self.get_is_complete_tr(req),
496 date_pertains_to=tr_qa(
497 req.wappstring(AS.DATE_PERTAINS_TO),
498 format_datetime(
499 self.date_pertains_to, DateFormat.LONG_DATE, default=None
500 ),
501 ),
502 category=tr_qa(
503 req.sstring(SS.CATEGORY) + " <sup>[1,2]</sup>",
504 self.get_description(req),
505 ),
506 psychotic_symptoms=tr_qa(
507 self.wxstring(req, "psychotic_symptoms") + " <sup>[2]</sup>",
508 get_present_absent_none(req, self.psychosis_present()),
509 ),
510 icd10_symptomatic_disclaimer=req.wappstring(
511 AS.ICD10_SYMPTOMATIC_DISCLAIMER
512 ),
513 )
515 h += self.text_row(req, "core")
516 for x in self.CORE_NAMES:
517 h += self.row_true_false(req, x)
519 h += self.text_row(req, "hypomania_mania")
520 for x in self.HYPOMANIA_MANIA_NAMES:
521 h += self.row_true_false(req, x)
523 h += self.text_row(req, "other_mania")
524 for x in self.MANIA_NAMES:
525 h += self.row_true_false(req, x)
527 h += self.text_row(req, "other_criteria")
528 for x in self.OTHER_CRITERIA_NAMES:
529 h += self.row_true_false(req, x)
531 h += subheading_spanning_two_columns(self.wxstring(req, "psychosis"))
532 for x in self.PSYCHOSIS_NAMES:
533 h += self.row_true_false(req, x)
535 h += f"""
536 </table>
537 <div class="{CssClass.FOOTNOTES}">
538 [1] Hypomania:
539 elevated/irritable mood
540 + sustained for ≥4 days
541 + at least 3 of the “other hypomania” symptoms
542 + some interference with functioning.
543 Mania:
544 elevated/irritable mood
545 + sustained for ≥7 days or hospital admission required
546 + at least 3 of the “other mania/hypomania” symptoms
547 (4 if mood only irritable)
548 + severe interference with functioning.
549 [2] ICD-10 nonpsychotic mania requires mania without
550 hallucinations/delusions.
551 ICD-10 psychotic mania requires mania plus
552 hallucinations/delusions other than those that are
553 “typically schizophrenic”.
554 ICD-10 does not clearly categorize mania with only
555 schizophreniform psychotic symptoms; however, Schneiderian
556 first-rank symptoms can occur in manic psychosis
557 (e.g. Conus P et al., 2004, PMID 15337330.).
558 </div>
559 {ICD10_COPYRIGHT_DIV}
560 """
561 return h