"""
camcops_server/tasks/aq.py
===============================================================================
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
**The Adult Autism Spectrum Quotient (AQ) Ages 16+ task.**
"""
from typing import Any, Dict, Iterable, List, Optional, Type
from cardinal_pythonlib.stringfunc import strseq
from sqlalchemy.sql.sqltypes import Integer
from camcops_server.cc_modules.cc_constants import CssClass
from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
from camcops_server.cc_modules.cc_db import add_multiple_columns
from camcops_server.cc_modules.cc_fhir import (
FHIRAnsweredQuestion,
FHIRAnswerType,
FHIRQuestionType,
)
from camcops_server.cc_modules.cc_html import answer, tr
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
from camcops_server.cc_modules.cc_text import SS
[docs]def to_csv(values: Iterable[Any]) -> str:
"""
Create a comma-separated string from iterable.
"""
return ", ".join(str(v) for v in values)
[docs]class Aq( # type: ignore[misc]
TaskHasPatientMixin,
Task,
):
__tablename__ = "aq"
shortname = "AQ"
prohibits_commercial = True
FIRST_Q = 1
LAST_Q = 50
PREFIX = "q"
MAX_AREA_SCORE = 10
MAX_SCORE = 50
# Questions where agreement indicates autistic-like traits.
@classmethod
def extend_columns(cls: Type["Aq"], **kwargs: Any) -> None:
add_multiple_columns(
cls,
cls.PREFIX,
cls.FIRST_Q,
cls.LAST_Q,
coltype=Integer,
minimum=0,
maximum=3,
comment_fmt=cls.PREFIX + "{n} - {s}",
comment_strings=[
# 1-5:
"prefer doing things with others",
"prefer doing things the same way",
"can create picture in mind",
"get strongly absorbed in one thing",
"notice small sounds",
# 6-10:
"notice car number plates",
"what I’ve said is impolite",
"can imagine what story characters look like",
"fascinated by dates",
"can keep track of conversations",
# 11-15:
"find social situations easy",
"notice details",
"prefer library to party",
"find making up stories easy",
"drawn more strongly to people",
# 16-20:
"upset if can't pursue strong interests",
"enjoy chit-chat",
"not easy for others to get a word in edgeways",
"fascinated by numbers",
"can't work out story characters’ intentions",
# 21-25:
"don’t enjoy fiction",
"hard to make new friends",
"notice patterns",
"prefer theatre to museum",
"not upset if daily routine disturbed",
# 26-30:
"don't know how to keep conversation going",
"easy to read between the lines",
"concentrate more on whole picture",
"can't remember phone numbers",
"don’t notice small changes",
# 31-35:
"can tell if person listening is bored",
"easy to do more than one thing",
"not sure when to speak on phone",
"enjoy doing things spontaneously",
"last to understand joke",
# 36-40:
"can work out thinking or feeling from face",
"can switch back after interruption",
"good at chit-chat",
"keep going on and on about the same thing",
"used to enjoy pretending games with other children",
# 41-45:
"like to collect information about categories of things",
"difficult to imagine being someone else",
"like to plan activities carefully",
"enjoy social occasions",
"difficult to work out people’s intentions",
# 46-50:
"new situations make me anxious",
"enjoy meeting new people",
"am a good diplomat",
"not very good at remembering people’s date of birth",
"easy to play pretending games with children",
],
)
# As listed in Baron-Cohen et al. (2001) [see refs in aq.rst], p7:
# 'Scoring the AQ: “Definitely agree” or “slightly agree” responses
# scored 1 point, on the following items: 1, 2, 4, 5, 6, 7, 9, 12, 13,
# 16, 18, 19, 20, 21, 22, 23, 26, 33, 35, 39, 41, 42, 43, 45, 46.
# “Definitely disagree” or “slightly disagree” responses scored 1 point,
# on the following items: 3, 8, 10, 11, 14, 15, 17, 24, 25, 27, 28, 29,
# 30, 31, 32, 34, 36, 37, 38, 40, 44, 47, 48, 49, 50.'
# HOWEVER, there is likely an error here in the published paper:
# Baron-Cohen et al. (2001) list Q1 as an "agree" question, but
# agreement there is a preference for doing things with others versus on
# one's own, so disagreement would be the more autistic-like answer (e.g.
# per WHO ICD-10 criteria for F84.1). The ARC's scoring sheet lists Q1 as a
# "disagree" question.
AGREE_SCORING_QUESTIONS = [
2,
4,
5,
6,
7,
9,
12,
13,
16,
18,
19,
20,
21,
22,
23,
26,
33,
35,
39,
41,
42,
43,
45,
46,
]
# Internal coding (not scoring) -- in the order on the questionnaire:
DEFINITELY_AGREE = 0
SLIGHTLY_AGREE = 1
SLIGHTLY_DISAGREE = 2
DEFINITELY_DISAGREE = 3
AGREE_OPTIONS = [DEFINITELY_AGREE, SLIGHTLY_AGREE]
DISAGREE_OPTIONS = [SLIGHTLY_DISAGREE, DEFINITELY_DISAGREE]
ALL_FIELD_NAMES = strseq(PREFIX, FIRST_Q, LAST_Q)
ALL_QUESTIONS = range(FIRST_Q, LAST_Q + 1)
# Areas (domains): see Baron-Cohen et al. (2001), p6.
SOCIAL_SKILL_QUESTIONS = [1, 11, 13, 15, 22, 36, 44, 45, 47, 48]
ATTENTION_SWITCHING_QUESTIONS = [2, 4, 10, 16, 25, 32, 34, 37, 43, 46]
ATTENTION_TO_DETAIL_QUESTIONS = [5, 6, 9, 12, 19, 23, 28, 29, 30, 49]
COMMUNICATION_QUESTIONS = [7, 17, 18, 26, 27, 31, 33, 35, 38, 39]
IMAGINATION_QUESTIONS = [3, 8, 14, 20, 21, 24, 40, 41, 42, 50]
[docs] @staticmethod
def longname(req: CamcopsRequest) -> str:
_ = req.gettext
return _("Adult Autism Spectrum Quotient")
[docs] def is_complete(self) -> bool:
# noinspection PyUnresolvedReferences
if self.any_fields_none(self.ALL_FIELD_NAMES):
return False
return True
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
return [
CtvInfo(
content=(
f"{req.sstring(SS.TOTAL_SCORE)} "
f"{self.score()}/{self.MAX_SCORE}"
)
)
]
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
mas = self.MAX_AREA_SCORE
return self.standard_task_summary_fields() + [
SummaryElement(
name="total",
coltype=Integer(),
value=self.score(),
comment=f"Total score (/{self.MAX_SCORE})",
),
SummaryElement(
name="social_skill",
coltype=Integer(),
value=self.social_skill_score(),
comment=f"Social skill domain score (/{mas})",
),
SummaryElement(
name="attention_switching",
coltype=Integer(),
value=self.attention_switching_score(),
comment=f"Attention switching domain score (/{mas})",
),
SummaryElement(
name="attention_to_detail",
coltype=Integer(),
value=self.attention_to_detail_score(),
comment=f"Attention to detail domain score (/{mas})",
),
SummaryElement(
name="communication",
coltype=Integer(),
value=self.communication_score(),
comment=f"Communication domain score (/{mas})",
),
SummaryElement(
name="imagination",
coltype=Integer(),
value=self.imagination_score(),
comment=f"Imagination domain score (/{mas})",
),
]
def score(self) -> Optional[int]:
return self.questions_score(self.ALL_QUESTIONS)
def social_skill_score(self) -> Optional[int]:
return self.questions_score(self.SOCIAL_SKILL_QUESTIONS)
def attention_switching_score(self) -> Optional[int]:
return self.questions_score(self.ATTENTION_SWITCHING_QUESTIONS)
def attention_to_detail_score(self) -> Optional[int]:
return self.questions_score(self.ATTENTION_TO_DETAIL_QUESTIONS)
def communication_score(self) -> Optional[int]:
return self.questions_score(self.COMMUNICATION_QUESTIONS)
def imagination_score(self) -> Optional[int]:
return self.questions_score(self.IMAGINATION_QUESTIONS)
def questions_score(self, q_nums: Iterable[int]) -> Optional[int]:
total = 0
for q_num in q_nums:
score = self.question_score(q_num)
if score is None:
return None
total += score
return total
[docs] def question_score(self, q_num: int) -> Optional[int]:
"""
Returns 1 if the answer reflects autistic-like behaviour, mildly or
strongly (per Baron-Cohen et al. 2001, p6). Returns 0 for the opposite.
Returns None for no answer or an invalid answer.
"""
q_field = self.PREFIX + str(q_num)
a = getattr(self, q_field)
if a is None:
return None
if q_num in self.AGREE_SCORING_QUESTIONS:
# Questions where agreement indicates autistic-like traits
if a in self.AGREE_OPTIONS:
return 1
elif a in self.DISAGREE_OPTIONS:
return 0
else:
# Shouldn't happen, but safety check
return None
else:
# Questions where disagreement indicates autistic-like traits
if a in self.AGREE_OPTIONS:
return 0
elif a in self.DISAGREE_OPTIONS:
return 1
else:
# Shouldn't happen, but safety check
return None
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
rows = self.get_task_html_rows(req)
html = """
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
{tr_is_complete}
{total_score}
{social_skill_score}
{attention_switching_score}
{attention_to_detail_score}
{communication_score}
{imagination_score}
</table>
</div>
<table class="{CssClass.TASKDETAIL}">
{rows}
</table>
<div class="{CssClass.FOOTNOTES}">
[1] Questions {social_skill_q_nums}.
[2] Questions {attention_switching_q_nums}.
[3] Questions {attention_to_detail_q_nums}.
[4] Questions {communication_q_nums}.
[5] Questions {imagination_q_nums}.
</div>
""".format(
CssClass=CssClass,
tr_is_complete=self.get_is_complete_tr(req),
total_score=tr(
req.sstring(SS.TOTAL_SCORE),
answer(self.score()) + f" / {self.MAX_SCORE}",
),
social_skill_score=tr(
self.wxstring(req, "social_skill_score") + " <sup>[1]</sup>",
answer(self.social_skill_score())
+ f" / {self.MAX_AREA_SCORE}",
),
attention_switching_score=tr(
self.wxstring(req, "attention_switching_score")
+ " <sup>[2]</sup>",
answer(self.attention_switching_score())
+ f" / {self.MAX_AREA_SCORE}",
),
attention_to_detail_score=tr(
self.wxstring(req, "attention_to_detail_score")
+ " <sup>[3]</sup>",
answer(self.attention_to_detail_score())
+ f" / {self.MAX_AREA_SCORE}",
),
communication_score=tr(
self.wxstring(req, "communication_score") + " <sup>[4]</sup>",
answer(self.communication_score())
+ f" / {self.MAX_AREA_SCORE}",
),
imagination_score=tr(
self.wxstring(req, "imagination_score") + " <sup>[5]</sup>",
answer(self.imagination_score()) + f" / {self.MAX_AREA_SCORE}",
),
social_skill_q_nums=to_csv(self.SOCIAL_SKILL_QUESTIONS),
attention_switching_q_nums=to_csv(
self.ATTENTION_SWITCHING_QUESTIONS
),
attention_to_detail_q_nums=to_csv(
self.ATTENTION_TO_DETAIL_QUESTIONS
),
communication_q_nums=to_csv(self.COMMUNICATION_QUESTIONS),
imagination_q_nums=to_csv(self.IMAGINATION_QUESTIONS),
rows=rows,
)
return html
def get_task_html_rows(self, req: CamcopsRequest) -> str:
_ = req.gettext
score_text = _("Score")
header = f"""
<tr>
<th width="70%">Statement</th>
<th width="20%">Answer</th>
<th width="10%">{score_text}</th>
</tr>
"""
return header + self.get_task_html_rows_for_range(
req, self.FIRST_Q, self.LAST_Q
)
def get_task_html_rows_for_range(
self, req: CamcopsRequest, first_q: int, last_q: int
) -> str:
rows = ""
for q_num in range(first_q, last_q + 1):
field = self.PREFIX + str(q_num)
question_cell = f"{q_num}. {self.xstring(req, field)}"
score = self.question_score(q_num)
rows += tr(
question_cell,
answer(self.get_answer_cell(req, q_num)),
score,
)
return rows
def get_answer_cell(
self, req: CamcopsRequest, q_num: int
) -> Optional[str]:
q_field = self.PREFIX + str(q_num)
response = getattr(self, q_field)
if response is None:
return response
return self.wxstring(req, f"option_{response}")
[docs] def get_fhir_questionnaire(
self, req: CamcopsRequest
) -> List[FHIRAnsweredQuestion]:
items = [] # type: List[FHIRAnsweredQuestion]
options = {} # type: Dict[int, str]
for index in range(4):
options[index] = self.wxstring(req, f"option_{index}")
for q_field in self.ALL_FIELD_NAMES:
items.append(
FHIRAnsweredQuestion(
qname=q_field,
qtext=self.xstring(req, q_field),
qtype=FHIRQuestionType.CHOICE,
answer_type=FHIRAnswerType.INTEGER,
answer=getattr(self, q_field),
answer_options=options,
)
)
return items
# No SNOMED codes for the AQ as of 2024-06-26.