Coverage for tasks/icd10manic.py : 36%

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