"""
camcops_server/tasks/edeq.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/>.
===============================================================================
**Eating Disorder Examination Questionnaire (EDE-Q 6.0) task.**
"""
import statistics
from typing import Any, Dict, List, Optional, Type, Tuple
from cardinal_pythonlib.stringfunc import strnumlist, strseq
from sqlalchemy import Column
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.sql.sqltypes import Boolean, Float, Integer
from camcops_server.cc_modules.cc_constants import CssClass
from camcops_server.cc_modules.cc_db import add_multiple_columns
from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
from camcops_server.cc_modules.cc_text import SS
from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
class EdeqMetaclass(DeclarativeMeta):
def __init__(
cls: Type["Edeq"],
name: str,
bases: Tuple[Type, ...],
classdict: Dict[str, Any],
) -> None:
add_multiple_columns(
cls,
"q",
1,
12,
coltype=Integer,
minimum=0,
maximum=6,
comment_fmt="Q{n} - {s}",
comment_strings=[
"days limit the amount of food 0-6 (no days - every day)",
"days long periods without eating 0-6 (no days - every day)",
"days exclude from diet 0-6 (no days - every day)",
"days follow rules 0-6 (no days - every day)",
"days desire empty stomach 0-6 (no days - every day)",
"days desire flat stomach 0-6 (no days - every day)",
"days thinking about food 0-6 (no days - every day)",
"days thinking about shape 0-6 (no days - every day)",
"days fear losing control 0-6 (no days - every day)",
"days fear weight gain 0-6 (no days - every day)",
"days felt fat 0-6 (no days - every day)",
"days desire lose weight 0-6 (no days - every day)",
],
)
add_multiple_columns(
cls,
"q",
13,
18,
coltype=Integer,
comment_fmt="Q{n} - {s}",
comment_strings=[
"times eaten unusually large amount of food",
"times sense lost control",
"days episodes of overeating",
"times made self sick",
"times taken laxatives",
"times exercised in driven or compulsive way",
],
)
add_multiple_columns(
cls,
"q",
19,
21,
coltype=Integer,
minimum=0,
maximum=6,
comment_fmt="Q{n} - {s}",
comment_strings=[
"days eaten in secret (no days - every day)",
"times felt guilty (none of the times - every time)",
"concern about people seeing you eat (not at all - markedly)",
],
)
add_multiple_columns(
cls,
"q",
22,
28,
coltype=Integer,
minimum=0,
maximum=6,
comment_fmt="Q{n} - {s}",
comment_strings=[
"weight influenced how you judge self (not at all - markedly)",
"shape influenced how you judge self (not at all - markedly)",
"upset if asked to weigh self (not at all - markedly)",
"dissatisfied with weight (not at all - markedly)",
"dissatisfied with shape (not at all - markedly)",
"uncomfortable seeing body (not at all - markedly)",
"uncomfortable others seeing shape (not at all - markedly)",
],
)
setattr(
cls,
"mass_kg",
Column("mass_kg", Float, comment="Mass (kg)"),
)
setattr(
cls,
"height_m",
Column("height_m", Float, comment="Height (m)"),
)
setattr(
cls,
"num_periods_missed",
Column(
"num_periods_missed",
Integer,
comment="Number of periods missed",
),
)
setattr(
cls,
"pill",
Column(
"pill", Boolean, comment="Taking the (oral contraceptive) pill"
),
)
super().__init__(name, bases, classdict)
[docs]class Edeq(TaskHasPatientMixin, Task, metaclass=EdeqMetaclass):
__tablename__ = "edeq"
shortname = "EDE-Q"
provides_trackers = True
N_QUESTIONS = 28
MEASUREMENT_FIELD_NAMES = ["mass_kg", "height_m"]
COMMON_FIELD_NAMES = strseq("q", 1, N_QUESTIONS) + MEASUREMENT_FIELD_NAMES
FEMALE_FIELD_NAMES = ["num_periods_missed", "pill"]
RESTRAINT_Q_NUMS = [1, 2, 3, 4, 5]
RESTRAINT_Q_STR = ", ".join(str(q) for q in RESTRAINT_Q_NUMS)
RESTRAINT_FIELD_NAMES = strnumlist("q", RESTRAINT_Q_NUMS)
EATING_CONCERN_Q_NUMS = [7, 9, 19, 20, 21]
EATING_CONCERN_Q_STR = ", ".join(str(q) for q in EATING_CONCERN_Q_NUMS)
EATING_CONCERN_FIELD_NAMES = strnumlist("q", EATING_CONCERN_Q_NUMS)
SHAPE_CONCERN_Q_NUMS = [6, 8, 10, 11, 23, 26, 27, 28]
SHAPE_CONCERN_Q_STR = ", ".join(str(q) for q in SHAPE_CONCERN_Q_NUMS)
SHAPE_CONCERN_FIELD_NAMES = strnumlist("q", SHAPE_CONCERN_Q_NUMS)
WEIGHT_CONCERN_Q_NUMS = [8, 12, 22, 24, 25]
WEIGHT_CONCERN_Q_STR = ", ".join(str(q) for q in WEIGHT_CONCERN_Q_NUMS)
WEIGHT_CONCERN_FIELD_NAMES = strnumlist("q", WEIGHT_CONCERN_Q_NUMS)
[docs] @staticmethod
def longname(req: CamcopsRequest) -> str:
_ = req.gettext
return _("Eating Disorder Examination Questionnaire")
[docs] def is_complete(self) -> bool:
if self.any_fields_none(self.COMMON_FIELD_NAMES):
return False
if self.patient.sex == "F" and self.any_fields_none(
self.FEMALE_FIELD_NAMES
):
return False
return True
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
return [
TrackerInfo(
value=self.global_score(),
plot_label="EDE-Q global score",
axis_label="Global score (0–6)",
axis_min=-0.5,
axis_max=6.5,
),
]
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
score_range = "[0–6]"
rows = ""
for q_num in range(1, self.N_QUESTIONS + 1):
field = "q" + str(q_num)
question_cell = self.xstring(req, field)
rows += tr_qa(question_cell, self.get_answer_cell(req, q_num))
mass = getattr(self, "mass_kg")
if mass is not None:
mass = f"{mass} kg"
height = getattr(self, "height_m")
if height is not None:
height = f"{height} m"
rows += tr_qa(self.xstring(req, "mass_kg"), mass)
rows += tr_qa(self.xstring(req, "height_m"), height)
if self.patient.is_female():
for field in self.FEMALE_FIELD_NAMES:
rows += tr_qa(self.xstring(req, field), getattr(self, field))
html = """
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
{tr_is_complete}
{global_score}
{restraint_score}
{eating_concern_score}
{shape_concern_score}
{weight_concern_score}
</table>
</div>
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="60%">Question</th>
<th width="40%">Score</th>
</tr>
{rows}
</table>
<div class="{CssClass.FOOTNOTES}">
[1] Mean of four subscales.
[2] Mean of questions {restraint_q_nums}.
[3] Mean of questions {eating_concern_q_nums}.
[4] Mean of questions {shape_concern_q_nums}.
[5] Mean of questions {weight_concern_q_nums}.
</div>
""".format(
CssClass=CssClass,
tr_is_complete=self.get_is_complete_tr(req),
global_score=tr(
req.sstring(SS.GLOBAL_SCORE) + " <sup>[1]</sup>",
f"{answer(self.global_score())} {score_range}",
),
restraint_score=tr(
self.wxstring(req, "restraint") + " <sup>[2]</sup>",
f"{answer(self.restraint())} {score_range}",
),
eating_concern_score=tr(
self.wxstring(req, "eating_concern") + " <sup>[3]</sup>",
f"{answer(self.eating_concern())} {score_range}",
),
shape_concern_score=tr(
self.wxstring(req, "shape_concern") + " <sup>[4]</sup>",
f"{answer(self.shape_concern())} {score_range}",
),
weight_concern_score=tr(
self.wxstring(req, "weight_concern") + " <sup>[5]</sup>",
f"{answer(self.weight_concern())} {score_range}",
),
rows=rows,
restraint_q_nums=self.RESTRAINT_Q_STR,
eating_concern_q_nums=self.EATING_CONCERN_Q_STR,
shape_concern_q_nums=self.SHAPE_CONCERN_Q_STR,
weight_concern_q_nums=self.WEIGHT_CONCERN_Q_STR,
)
return html
def get_answer_cell(
self, req: CamcopsRequest, q_num: int
) -> Optional[str]:
q_field = "q" + str(q_num)
score = getattr(self, q_field)
if score is None or (13 <= q_num <= 18):
return score
meaning = self.get_score_meaning(req, q_num, score)
answer_cell = f"{score} [{meaning}]"
return answer_cell
def get_score_meaning(
self, req: CamcopsRequest, q_num: int, score: int
) -> str:
if q_num <= 12 or q_num == 19:
return self.wxstring(req, f"days_option_{score}")
if q_num == 20:
return self.wxstring(req, f"freq_option_{score}")
if score % 2 == 1:
previous = self.wxstring(req, f"how_much_option_{score-1}")
next_ = self.wxstring(req, f"how_much_option_{score+1}")
return f"{previous}—{next_}"
return self.wxstring(req, f"how_much_option_{score}")
def restraint(self) -> Optional[float]:
return self.subscale(self.RESTRAINT_FIELD_NAMES)
def eating_concern(self) -> Optional[float]:
return self.subscale(self.EATING_CONCERN_FIELD_NAMES)
def shape_concern(self) -> Optional[float]:
return self.subscale(self.SHAPE_CONCERN_FIELD_NAMES)
def weight_concern(self) -> Optional[float]:
return self.subscale(self.WEIGHT_CONCERN_FIELD_NAMES)
def subscale(self, field_names: List[str]) -> Optional[float]:
if self.any_fields_none(field_names):
return None
return self.mean_fields(field_names)
def global_score(self) -> Optional[float]:
subscales = [
self.restraint(),
self.eating_concern(),
self.shape_concern(),
self.weight_concern(),
]
if None in subscales:
return None
return statistics.mean(subscales)