class IsaaqEd(IsaaqCommon, metaclass=IsaaqEdMetaclass): __tablename__ = "isaaqed" shortname = "ISAAQ-ED" Q_PREFIX = "e" FIRST_Q = 11 LAST_Q = 20 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q) @staticmethod def longname(req: CamcopsRequest) -> str: _ = req.gettext return _("Internet Severity and Activities Addiction Questionnaire, " "Eating Disorders Appendix") def is_complete(self) -> bool: if self.any_fields_none(self.ALL_FIELD_NAMES): return False return True def get_task_html_rows(self, req: CamcopsRequest) -> str: header = """ <tr> <th width="70%">{title}</th> <th width="30%">{scale}</th> </tr> """.format( title=self.xstring(req, "grid_title"), scale=self.xstring(req, "scale"), ) return header + self.get_task_html_rows_for_range( req, self.Q_PREFIX, self.FIRST_Q, self.LAST_Q)
class Ctqsf(TaskHasPatientMixin, Task): """ Server implementation of the CTQ-SF task. """ __tablename__ = "ctqsf" shortname = "CTQ-SF" provides_trackers = False # todo: Ctqsf fields N_QUESTIONS = 28 QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Childhood Trauma Questionnaire, Short Form") # noinspection PyMethodParameters @classproperty def minimum_client_version(cls) -> Version: return Version("2.2.8") def is_complete(self) -> bool: return self.all_fields_not_none(self.QUESTION_FIELDNAMES) def get_task_html(self, req: CamcopsRequest) -> str: return "" # todo: IMPLEMENT Ctqsf
def basdai(self) -> Optional[float]: """ Calculating the BASDAI A. Add scores for questions 1 – 4 B. Calculate the mean for questions 5 and 6 C. Add A and B and divide by 5 The higher the BASDAI score, the more severe the patient’s disability due to their AS. """ if not self.is_complete(): return None score_a_field_names = strseq("q", 1, 4) score_b_field_names = strseq("q", 5, 6) a = sum([getattr(self, q) for q in score_a_field_names]) b = statistics.mean([getattr(self, q) for q in score_b_field_names]) return (a + b) / 5
class Isaaq(IsaaqCommon, metaclass=IsaaqMetaclass): __tablename__ = "isaaq" shortname = "ISAAQ" A_PREFIX = "a" B_PREFIX = "b" FIRST_Q = 1 LAST_A_Q = 15 LAST_B_Q = 10 ALL_FIELD_NAMES = strseq(A_PREFIX, FIRST_Q, LAST_A_Q) + strseq( B_PREFIX, FIRST_Q, LAST_B_Q) @staticmethod def longname(req: CamcopsRequest) -> str: _ = req.gettext return _("Internet Severity and Activities Addiction Questionnaire") def get_task_html_rows(self, req: CamcopsRequest) -> str: header_format = """ <tr> <th width="70%">{title}</th> <th width="30%">{scale}</th> </tr> """ a_header = header_format.format( title=self.xstring(req, "a_title"), scale=self.xstring(req, "scale"), ) b_header = header_format.format( title=self.xstring(req, "b_title"), scale=self.xstring(req, "scale"), ) return (a_header + self.get_task_html_rows_for_range( req, self.A_PREFIX, self.FIRST_Q, self.LAST_A_Q) + b_header + self.get_task_html_rows_for_range( req, self.B_PREFIX, self.FIRST_Q, self.LAST_B_Q))
class Badls(TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=BadlsMetaclass): """ Server implementation of the BADLS task. """ __tablename__ = "badls" shortname = "BADLS" provides_trackers = True SCORING = {"a": 0, "b": 1, "c": 2, "d": 3, "e": 0} NQUESTIONS = 20 QUESTION_SNIPPETS = [ "food", # 1 "eating", "drink", "drinking", "dressing", # 5 "hygiene", "teeth", "bath/shower", "toilet/commode", "transfers", # 10 "mobility", "orientation: time", "orientation: space", "communication", "telephone", # 15 "hosuework/gardening", "shopping", "finances", "games/hobbies", "transport", # 20 ] TASK_FIELDS = strseq("q", 1, NQUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Bristol Activities of Daily Living Scale") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment="Total score (/ 48)", ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content="BADLS total score {}/60 (lower is better)".format( self.total_score())) ] def score(self, q: str) -> int: text_value = getattr(self, q) return self.SCORING.get(text_value, 0) def total_score(self) -> int: return sum(self.score(q) for q in self.TASK_FIELDS) def is_complete(self) -> bool: return (self.field_contents_valid() and self.is_respondent_complete() and self.all_fields_not_none(self.TASK_FIELDS)) def get_task_html(self, req: CamcopsRequest) -> str: q_a = "" for q in range(1, self.NQUESTIONS + 1): fieldname = "q" + str(q) qtext = self.wxstring(req, fieldname) # happens to be the same avalue = getattr(self, "q" + str(q)) atext = (self.wxstring(req, "q{}_{}".format(q, avalue)) if q is not None else None) score = self.score(fieldname) q_a += tr(qtext, answer(atext), score) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} <tr> <td>Total score (0–60, higher worse)</td> <td>{answer(self.total_score())}</td> </td> </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="30%">Question</th> <th width="50%">Answer <sup>[1]</sup></th> <th width="20%">Score</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Scored a = 0, b = 1, c = 2, d = 3, e = 0. </div> {DATA_COLLECTION_UNLESS_UPGRADED_DIV} """ def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: # The BADLS is ALWAYS carer-rated, so it's appropriate to put the # SNOMED-CT codes in. codes = [ SnomedExpression( req.snomed(SnomedLookup.BADLS_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.BADLS_SCALE), {req.snomed(SnomedLookup.BADLS_SCORE): self.total_score()}, )) return codes
18: "changes in appetite", # decrease or increase 19: "concentration difficulty", 20: "tiredness or fatigue", 21: "loss of interest in sex", } SCALE_BDI_I = "BDI-I" # must match client SCALE_BDI_IA = "BDI-IA" # must match client SCALE_BDI_II = "BDI-II" # must match client TOPICS_BY_SCALE = { SCALE_BDI_I: BDI_I_QUESTION_TOPICS, SCALE_BDI_IA: BDI_IA_QUESTION_TOPICS, SCALE_BDI_II: BDI_II_QUESTION_TOPICS, } NQUESTIONS = 21 TASK_SCORED_FIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = NQUESTIONS * 3 SUICIDALITY_QNUM = 9 # Q9 in all versions of the BDI (I, IA, II) SUICIDALITY_FN = "q9" # fieldname CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS = [4, 15, 16, 18, 19, 20, 21] CUSTOM_SOMATIC_KHANDAKER_BDI_II_FIELDS = Task.fieldnames_from_list( "q", CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS) # ============================================================================= # BDI (crippled) # ============================================================================= class BdiMetaclass(DeclarativeMeta): # noinspection PyInitNewSignature def __init__(
class Factg(TaskHasPatientMixin, Task, metaclass=FactgMetaclass): """ Server implementation of the Fact-G task. """ __tablename__ = "factg" shortname = "FACT-G" longname = "Functional Assessment of Cancer Therapy — General" provides_trackers = True N_QUESTIONS_PHYSICAL = 7 N_QUESTIONS_SOCIAL = 7 N_QUESTIONS_EMOTIONAL = 6 N_QUESTIONS_FUNCTIONAL = 7 MAX_SCORE_PHYSICAL = 28 MAX_SCORE_SOCIAL = 28 MAX_SCORE_EMOTIONAL = 24 MAX_SCORE_FUNCTIONAL = 28 N_ALL = (N_QUESTIONS_PHYSICAL + N_QUESTIONS_SOCIAL + N_QUESTIONS_EMOTIONAL + N_QUESTIONS_FUNCTIONAL) MAX_SCORE_TOTAL = N_ALL * MAX_QSCORE PHYSICAL_PREFIX = "p_q" SOCIAL_PREFIX = "s_q" EMOTIONAL_PREFIX = "e_q" FUNCTIONAL_PREFIX = "f_q" QUESTIONS_PHYSICAL = strseq(PHYSICAL_PREFIX, 1, N_QUESTIONS_PHYSICAL) QUESTIONS_SOCIAL = strseq(SOCIAL_PREFIX, 1, N_QUESTIONS_SOCIAL) QUESTIONS_EMOTIONAL = strseq(EMOTIONAL_PREFIX, 1, N_QUESTIONS_EMOTIONAL) QUESTIONS_FUNCTIONAL = strseq(FUNCTIONAL_PREFIX, 1, N_QUESTIONS_FUNCTIONAL) GROUPS = [ FactgGroupInfo("h1", PHYSICAL_PREFIX, QUESTIONS_PHYSICAL, "physical_wellbeing", "Physical wellbeing subscore", MAX_SCORE_PHYSICAL, reverse_score_all=True), FactgGroupInfo("h2", SOCIAL_PREFIX, QUESTIONS_SOCIAL, "social_family_wellbeing", "Social/family wellbeing subscore", MAX_SCORE_SOCIAL), FactgGroupInfo("h3", EMOTIONAL_PREFIX, QUESTIONS_EMOTIONAL, "emotional_wellbeing", "Emotional wellbeing subscore", MAX_SCORE_EMOTIONAL, reverse_score_all_but_q2=True), FactgGroupInfo("h4", FUNCTIONAL_PREFIX, QUESTIONS_FUNCTIONAL, "functional_wellbeing", "Functional wellbeing subscore", MAX_SCORE_FUNCTIONAL), ] OPTIONAL_Q = "s_q7" ignore_s_q7 = CamcopsColumn("ignore_s_q7", Boolean, permitted_value_checker=BIT_CHECKER) def is_complete(self) -> bool: questions_social = self.QUESTIONS_SOCIAL.copy() if self.ignore_s_q7: questions_social.remove(self.OPTIONAL_Q) all_qs = [ self.QUESTIONS_PHYSICAL, questions_social, self.QUESTIONS_EMOTIONAL, self.QUESTIONS_FUNCTIONAL ] for qlist in all_qs: if not self.are_all_fields_complete(qlist): return False if not self.field_contents_valid(): return False return True def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="FACT-G total score (rating well-being)", axis_label="Total score".format(self.MAX_SCORE_TOTAL), axis_min=-0.5, axis_max=self.MAX_SCORE_TOTAL + 0.5, axis_ticks=[ TrackerAxisTick(108, "108"), TrackerAxisTick(100, "100"), TrackerAxisTick(80, "80"), TrackerAxisTick(60, "60"), TrackerAxisTick(40, "40"), TrackerAxisTick(20, "20"), TrackerAxisTick(0, "0"), ], horizontal_lines=[80, 60, 40, 20], ) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: elements = self.standard_task_summary_fields() for info in self.GROUPS: subscore = info.subscore(self) elements.append( SummaryElement(name=info.summary_fieldname, coltype=Float(), value=subscore, comment="{} (out of {})".format( info.summary_description, info.max_score))) elements.append( SummaryElement(name="total_score", coltype=Float(), value=self.total_score(), comment="Total score (out of {})".format( self.MAX_SCORE_TOTAL))) return elements def subscores(self) -> List[float]: sscores = [] for info in self.GROUPS: sscores.append(info.subscore(self)) return sscores def total_score(self) -> float: return sum(self.subscores()) def get_task_html(self, req: CamcopsRequest) -> str: answers = { None: None, 0: "0 — " + self.wxstring(req, "a0"), 1: "1 — " + self.wxstring(req, "a1"), 2: "2 — " + self.wxstring(req, "a2"), 3: "3 — " + self.wxstring(req, "a3"), 4: "4 — " + self.wxstring(req, "a4"), } subscore_html = "" answer_html = "" for info in self.GROUPS: heading = self.wxstring(req, info.heading_xstring_name) subscore = info.subscore(self) subscore_html += tr(heading, (answer(round(subscore, DISPLAY_DP)) + " / {}".format(info.max_score))) answer_html += subheading_spanning_two_columns(heading) for q in info.fieldnames: if q == self.OPTIONAL_Q: # insert additional row answer_html += tr_qa(self.xstring(req, "prefer_no_answer"), self.ignore_s_q7) answer_val = getattr(self, q) answer_html += tr_qa(self.wxstring(req, q), get_from_dict(answers, answer_val)) tscore = round(self.total_score(), DISPLAY_DP) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {subscore_html} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer</th> </tr> {answer_html} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.wappstring("total_score"), answer(tscore) + " / {}".format(self.MAX_SCORE_TOTAL)), subscore_html=subscore_html, answer_html=answer_html, ) h += """ </table> """ return h
class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass): """ Server implementation of the CESD task. """ __tablename__ = "cesd" shortname = "CESD" provides_trackers = True extrastring_taskname = "cesd" N_QUESTIONS = 20 N_ANSWERS = 4 DEPRESSION_RISK_THRESHOLD = 16 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS) TASK_FIELDS = SCORED_FIELDS MIN_SCORE = 0 MAX_SCORE = 3 * N_QUESTIONS REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Center for Epidemiologic Studies Depression Scale") # noinspection PyMethodParameters @classproperty def minimum_client_version(cls) -> Version: return Version("2.2.8") def is_complete(self) -> bool: return (self.all_fields_not_none(self.TASK_FIELDS) and self.field_contents_valid()) def total_score(self) -> int: # Need to store values as per original then flip here total = 0 for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1): score = getattr(self, fieldname) if score is None: continue if qnum in self.REVERSE_SCORED_QUESTIONS: total += 3 - score else: total += score return total def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: line_step = 20 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 # noinspection PyTypeChecker return [ TrackerInfo( value=self.total_score(), plot_label="CESD total score", axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})", axis_min=self.MIN_SCORE - 0.5, axis_max=self.MAX_SCORE + 0.5, axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE, self.MAX_SCORE, step=line_step), horizontal_lines=equally_spaced_int( self.MIN_SCORE + line_step, self.MAX_SCORE - line_step, step=line_step, ) + [threshold_line], horizontal_labels=[ TrackerLabel( threshold_line, self.wxstring(req, "depression_or_risk_of"), ) ], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [CtvInfo(content=f"CESD total score {self.total_score()}")] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="depression_risk", coltype=Boolean(), value=self.has_depression_risk(), comment="Has depression or at risk of depression", ) ] def has_depression_risk(self) -> bool: return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() answer_dict = {None: None} for option in range(self.N_ANSWERS): answer_dict[option] = (str(option) + " – " + self.wxstring(req, "a" + str(option))) q_a = "" for q in range(1, self.N_QUESTIONS): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict(answer_dict, getattr(self, "q" + str(q))), ) tr_total_score = (tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (0–60)", score), ) tr_depression_or_risk_of = (tr_qa( self.wxstring(req, "depression_or_risk_of") + "? <sup>[1]</sup>", get_yes_no(req, self.has_depression_risk()), ), ) return f"""
class Wsas(TaskHasPatientMixin, Task, metaclass=WsasMetaclass): """ Server implementation of the WSAS task. """ __tablename__ = "wsas" shortname = "WSAS" provides_trackers = True retired_etc = Column( "retired_etc", Boolean, comment="Retired or choose not to have job for reason unrelated " "to problem", ) MIN_PER_Q = 0 MAX_PER_Q = 8 NQUESTIONS = 5 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS) Q2_TO_END = strseq("q", 2, NQUESTIONS) TASK_FIELDS = QUESTION_FIELDS + ["retired_etc"] MAX_IF_WORKING = MAX_PER_Q * NQUESTIONS MAX_IF_RETIRED = MAX_PER_Q * (NQUESTIONS - 1) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Work and Social Adjustment Scale") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="WSAS total score (lower is better)", axis_label=f"Total score (out of " f"{self.MAX_IF_RETIRED}–{self.MAX_IF_WORKING})", axis_min=-0.5, axis_max=self.MAX_IF_WORKING + 0.5, ) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment=f"Total score (/ {self.max_score()})", ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=f"WSAS total score " f"{self.total_score()}/{self.max_score()}") ] def total_score(self) -> int: return self.sum_fields( self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS) def max_score(self) -> int: return self.MAX_IF_RETIRED if self.retired_etc else self.MAX_IF_WORKING def is_complete(self) -> bool: return (self.all_fields_not_none( self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS) and self.field_contents_valid()) def get_task_html(self, req: CamcopsRequest) -> str: option_dict = {None: None} for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1): option_dict[a] = req.wappstring(AS.WSAS_A_PREFIX + str(a)) q_a = "" for q in range(1, self.NQUESTIONS + 1): a = getattr(self, "q" + str(q)) fa = get_from_dict(option_dict, a) if a is not None else None q_a += tr(self.wxstring(req, "q" + str(q)), answer(fa)) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} <tr> <td>Total score</td> <td>{answer(self.total_score())} / 40</td> </td> </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer</th> </tr> {tr_qa(self.wxstring(req, "q_retired_etc"), get_true_false(req, self.retired_etc))} </table> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer (0–8)</th> </tr> {q_a} </table> {DATA_COLLECTION_UNLESS_UPGRADED_DIV} """ # noinspection PyUnresolvedReferences def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression(req.snomed( SnomedLookup.WSAS_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): d = { req.snomed(SnomedLookup.WSAS_SCORE): self.total_score(), req.snomed(SnomedLookup.WSAS_HOME_MANAGEMENT_SCORE): self.q2, req.snomed(SnomedLookup.WSAS_SOCIAL_LEISURE_SCORE): self.q3, req.snomed(SnomedLookup.WSAS_PRIVATE_LEISURE_SCORE): self.q4, req.snomed(SnomedLookup.WSAS_RELATIONSHIPS_SCORE): self.q5, } if not self.retired_etc: d[req.snomed(SnomedLookup.WSAS_WORK_SCORE)] = self.q1 codes.append( SnomedExpression(req.snomed(SnomedLookup.WSAS_SCALE), d)) return codes
class Icd10SpecPD(TaskHasClinicianMixin, TaskHasPatientMixin, Task, metaclass=Icd10SpecPDMetaclass): """ Server implementation of the ICD10-PD task. """ __tablename__ = "icd10specpd" shortname = "ICD10-PD" longname = "ICD-10 criteria for specific personality disorders (F60)" date_pertains_to = Column("date_pertains_to", Date, comment="Date the assessment pertains to") comments = Column("comments", UnicodeText, comment="Clinician's comments") skip_paranoid = CamcopsColumn("skip_paranoid", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for paranoid PD?") skip_schizoid = CamcopsColumn("skip_schizoid", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for schizoid PD?") skip_dissocial = CamcopsColumn("skip_dissocial", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for dissocial PD?") skip_eu = CamcopsColumn( "skip_eu", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for emotionally unstable PD?") skip_histrionic = CamcopsColumn( "skip_histrionic", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for histrionic PD?") skip_anankastic = CamcopsColumn( "skip_anankastic", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for anankastic PD?") skip_anxious = CamcopsColumn("skip_anxious", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for anxious PD?") skip_dependent = CamcopsColumn("skip_dependent", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for dependent PD?") other_pd_present = CamcopsColumn( "other_pd_present", Boolean, permitted_value_checker=BIT_CHECKER, comment="Is another personality disorder present?") vignette = Column("vignette", UnicodeText, comment="Vignette") N_GENERAL = 6 N_GENERAL_1 = 4 N_PARANOID = 7 N_SCHIZOID = 9 N_DISSOCIAL = 6 N_EU = 10 N_EUPD_I = 5 N_HISTRIONIC = 6 N_ANANKASTIC = 8 N_ANXIOUS = 5 N_DEPENDENT = 6 GENERAL_FIELDS = strseq("g", 1, N_GENERAL) GENERAL_1_FIELDS = strseq("g1_", 1, N_GENERAL_1) PARANOID_FIELDS = strseq("paranoid", 1, N_PARANOID) SCHIZOID_FIELDS = strseq("schizoid", 1, N_SCHIZOID) DISSOCIAL_FIELDS = strseq("dissocial", 1, N_DISSOCIAL) EU_FIELDS = strseq("eu", 1, N_EU) EUPD_I_FIELDS = strseq("eu", 1, N_EUPD_I) # impulsive EUPD_B_FIELDS = strseq("eu", N_EUPD_I + 1, N_EU) # borderline HISTRIONIC_FIELDS = strseq("histrionic", 1, N_HISTRIONIC) ANANKASTIC_FIELDS = strseq("anankastic", 1, N_ANANKASTIC) ANXIOUS_FIELDS = strseq("anxious", 1, N_ANXIOUS) DEPENDENT_FIELDS = strseq("dependent", 1, N_DEPENDENT) def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE infolist = [ ctv_info_pd(req, self.wxstring(req, "meets_general_criteria"), self.has_pd()), ctv_info_pd(req, self.wxstring(req, "paranoid_pd_title"), self.has_paranoid_pd()), ctv_info_pd(req, self.wxstring(req, "schizoid_pd_title"), self.has_schizoid_pd()), ctv_info_pd(req, self.wxstring(req, "dissocial_pd_title"), self.has_dissocial_pd()), ctv_info_pd(req, self.wxstring(req, "eu_pd_i_title"), self.has_eupd_i()), ctv_info_pd(req, self.wxstring(req, "eu_pd_b_title"), self.has_eupd_b()), ctv_info_pd(req, self.wxstring(req, "histrionic_pd_title"), self.has_histrionic_pd()), ctv_info_pd(req, self.wxstring(req, "anankastic_pd_title"), self.has_anankastic_pd()), ctv_info_pd(req, self.wxstring(req, "anxious_pd_title"), self.has_anxious_pd()), ctv_info_pd(req, self.wxstring(req, "dependent_pd_title"), self.has_dependent_pd()) ] return infolist def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="meets_general_criteria", coltype=Boolean(), value=self.has_pd(), comment="Meets general criteria for personality disorder?"), SummaryElement(name="paranoid_pd", coltype=Boolean(), value=self.has_paranoid_pd(), comment="Meets criteria for paranoid PD?"), SummaryElement(name="schizoid_pd", coltype=Boolean(), value=self.has_schizoid_pd(), comment="Meets criteria for schizoid PD?"), SummaryElement(name="dissocial_pd", coltype=Boolean(), value=self.has_dissocial_pd(), comment="Meets criteria for dissocial PD?"), SummaryElement( name="eupd_i", coltype=Boolean(), value=self.has_eupd_i(), comment="Meets criteria for EUPD (impulsive type)?"), SummaryElement( name="eupd_b", coltype=Boolean(), value=self.has_eupd_b(), comment="Meets criteria for EUPD (borderline type)?"), SummaryElement(name="histrionic_pd", coltype=Boolean(), value=self.has_histrionic_pd(), comment="Meets criteria for histrionic PD?"), SummaryElement(name="anankastic_pd", coltype=Boolean(), value=self.has_anankastic_pd(), comment="Meets criteria for anankastic PD?"), SummaryElement(name="anxious_pd", coltype=Boolean(), value=self.has_anxious_pd(), comment="Meets criteria for anxious PD?"), SummaryElement(name="dependent_pd", coltype=Boolean(), value=self.has_dependent_pd(), comment="Meets criteria for dependent PD?"), ] # noinspection PyUnresolvedReferences def is_pd_excluded(self) -> bool: return (is_false(self.g1) or is_false(self.g2) or is_false(self.g3) or is_false(self.g4) or is_false(self.g5) or is_false(self.g6) or (self.are_all_fields_complete(self.GENERAL_1_FIELDS) and self.count_booleans(self.GENERAL_1_FIELDS) <= 1)) def is_complete_general(self) -> bool: return (self.are_all_fields_complete(self.GENERAL_FIELDS) and self.are_all_fields_complete(self.GENERAL_1_FIELDS)) def is_complete_paranoid(self) -> bool: return self.are_all_fields_complete(self.PARANOID_FIELDS) def is_complete_schizoid(self) -> bool: return self.are_all_fields_complete(self.SCHIZOID_FIELDS) def is_complete_dissocial(self) -> bool: return self.are_all_fields_complete(self.DISSOCIAL_FIELDS) def is_complete_eu(self) -> bool: return self.are_all_fields_complete(self.EU_FIELDS) def is_complete_histrionic(self) -> bool: return self.are_all_fields_complete(self.HISTRIONIC_FIELDS) def is_complete_anankastic(self) -> bool: return self.are_all_fields_complete(self.ANANKASTIC_FIELDS) def is_complete_anxious(self) -> bool: return self.are_all_fields_complete(self.ANXIOUS_FIELDS) def is_complete_dependent(self) -> bool: return self.are_all_fields_complete(self.DEPENDENT_FIELDS) # Meets criteria? These also return null for unknown. def has_pd(self) -> Optional[bool]: if self.is_pd_excluded(): return False if not self.is_complete_general(): return None return (self.all_true(self.GENERAL_FIELDS) and self.count_booleans(self.GENERAL_1_FIELDS) > 1) def has_paranoid_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_paranoid(): return None return self.count_booleans(self.PARANOID_FIELDS) >= 4 def has_schizoid_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_schizoid(): return None return self.count_booleans(self.SCHIZOID_FIELDS) >= 4 def has_dissocial_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_dissocial(): return None return self.count_booleans(self.DISSOCIAL_FIELDS) >= 3 # noinspection PyUnresolvedReferences def has_eupd_i(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_eu(): return None return (self.count_booleans(self.EUPD_I_FIELDS) >= 3 and self.eu2) def has_eupd_b(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_eu(): return None return (self.count_booleans(self.EUPD_I_FIELDS) >= 3 and self.count_booleans(self.EUPD_B_FIELDS) >= 2) def has_histrionic_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_histrionic(): return None return self.count_booleans(self.HISTRIONIC_FIELDS) >= 4 def has_anankastic_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_anankastic(): return None return self.count_booleans(self.ANANKASTIC_FIELDS) >= 4 def has_anxious_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_anxious(): return None return self.count_booleans(self.ANXIOUS_FIELDS) >= 4 def has_dependent_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_dependent(): return None return self.count_booleans(self.DEPENDENT_FIELDS) >= 4 def is_complete(self) -> bool: return (self.date_pertains_to is not None and (self.is_pd_excluded() or (self.is_complete_general() and (self.skip_paranoid or self.is_complete_paranoid()) and (self.skip_schizoid or self.is_complete_schizoid()) and (self.skip_dissocial or self.is_complete_dissocial()) and (self.skip_eu or self.is_complete_eu()) and (self.skip_histrionic or self.is_complete_histrionic()) and (self.skip_anankastic or self.is_complete_anankastic()) and (self.skip_anxious or self.is_complete_anxious()) and (self.skip_dependent or self.is_complete_dependent()))) and self.field_contents_valid()) def pd_heading(self, req: CamcopsRequest, wstringname: str) -> str: return """ <tr class="{CssClass.HEADING}"><td colspan="2">{s}</td></tr> """.format(CssClass=CssClass, s=self.wxstring(req, wstringname)) def pd_skiprow(self, req: CamcopsRequest, stem: str) -> str: return self.get_twocol_bool_row(req, "skip_" + stem, label=self.wxstring( req, "skip_this_pd")) def pd_subheading(self, req: CamcopsRequest, wstringname: str) -> str: return """ <tr class="{CssClass.SUBHEADING}"><td colspan="2">{s}</td></tr> """.format(CssClass=CssClass, s=self.wxstring(req, wstringname)) def pd_general_criteria_bits(self, req: CamcopsRequest) -> str: return """ <tr><td>{}</td><td><i><b>{}</b></i></td></tr> """.format(self.wxstring(req, "general_criteria_must_be_met"), get_yes_no_unknown(req, self.has_pd())) def pd_b_text(self, req: CamcopsRequest, wstringname: str) -> str: return """ <tr><td>{s}</td><td class="{CssClass.SUBHEADING}"></td></tr> """.format( CssClass=CssClass, s=self.wxstring(req, wstringname), ) def pd_basic_row(self, req: CamcopsRequest, stem: str, i: int) -> str: return self.get_twocol_bool_row_true_false( req, stem + str(i), self.wxstring(req, stem + str(i))) def standard_pd_html(self, req: CamcopsRequest, stem: str, n: int) -> str: html = self.pd_heading(req, stem + "_pd_title") html += self.pd_skiprow(req, stem) html += self.pd_general_criteria_bits(req) html += self.pd_b_text(req, stem + "_pd_B") for i in range(1, n + 1): html += self.pd_basic_row(req, stem, i) return html def get_task_html(self, req: CamcopsRequest) -> str: h = self.get_standard_clinician_comments_block(req, self.comments) h += """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> """.format(CssClass=CssClass, ) h += self.get_is_complete_tr(req) h += tr_qa( req.wappstring("date_pertains_to"), format_datetime(self.date_pertains_to, DateFormat.LONG_DATE, default=None)) h += tr_qa(self.wxstring(req, "meets_general_criteria"), get_yes_no_none(req, self.has_pd())) h += tr_qa(self.wxstring(req, "paranoid_pd_title"), get_yes_no_none(req, self.has_paranoid_pd())) h += tr_qa(self.wxstring(req, "schizoid_pd_title"), get_yes_no_none(req, self.has_schizoid_pd())) h += tr_qa(self.wxstring(req, "dissocial_pd_title"), get_yes_no_none(req, self.has_dissocial_pd())) h += tr_qa(self.wxstring(req, "eu_pd_i_title"), get_yes_no_none(req, self.has_eupd_i())) h += tr_qa(self.wxstring(req, "eu_pd_b_title"), get_yes_no_none(req, self.has_eupd_b())) h += tr_qa(self.wxstring(req, "histrionic_pd_title"), get_yes_no_none(req, self.has_histrionic_pd())) h += tr_qa(self.wxstring(req, "anankastic_pd_title"), get_yes_no_none(req, self.has_anankastic_pd())) h += tr_qa(self.wxstring(req, "anxious_pd_title"), get_yes_no_none(req, self.has_anxious_pd())) h += tr_qa(self.wxstring(req, "dependent_pd_title"), get_yes_no_none(req, self.has_dependent_pd())) h += """ </table> </div> <div> <p><i>Vignette:</i></p> <p>{vignette}</p> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> """.format( CssClass=CssClass, vignette=answer(ws.webify(self.vignette), default_for_blank_strings=True), ) # General h += subheading_spanning_two_columns(self.wxstring(req, "general")) h += self.get_twocol_bool_row_true_false(req, "g1", self.wxstring(req, "G1")) h += self.pd_b_text(req, "G1b") for i in range(1, Icd10SpecPD.N_GENERAL_1 + 1): h += self.get_twocol_bool_row_true_false( req, "g1_" + str(i), self.wxstring(req, "G1_" + str(i))) for i in range(2, Icd10SpecPD.N_GENERAL + 1): h += self.get_twocol_bool_row_true_false( req, "g" + str(i), self.wxstring(req, "G" + str(i))) # Paranoid, etc. h += self.standard_pd_html(req, "paranoid", Icd10SpecPD.N_PARANOID) h += self.standard_pd_html(req, "schizoid", Icd10SpecPD.N_SCHIZOID) h += self.standard_pd_html(req, "dissocial", Icd10SpecPD.N_DISSOCIAL) # EUPD is special h += self.pd_heading(req, "eu_pd_title") h += self.pd_skiprow(req, "eu") h += self.pd_general_criteria_bits(req) h += self.pd_subheading(req, "eu_pd_i_title") h += self.pd_b_text(req, "eu_pd_i_B") for i in range(1, Icd10SpecPD.N_EUPD_I + 1): h += self.pd_basic_row(req, "eu", i) h += self.pd_subheading(req, "eu_pd_b_title") h += self.pd_b_text(req, "eu_pd_b_B") for i in range(Icd10SpecPD.N_EUPD_I + 1, Icd10SpecPD.N_EU + 1): h += self.pd_basic_row(req, "eu", i) # Back to plain ones h += self.standard_pd_html(req, "histrionic", Icd10SpecPD.N_HISTRIONIC) h += self.standard_pd_html(req, "anankastic", Icd10SpecPD.N_ANANKASTIC) h += self.standard_pd_html(req, "anxious", Icd10SpecPD.N_ANXIOUS) h += self.standard_pd_html(req, "dependent", Icd10SpecPD.N_DEPENDENT) # Done h += """ </table> """ + ICD10_COPYRIGHT_DIV return h
class Audit(TaskHasPatientMixin, Task, metaclass=AuditMetaclass): """ Server implementation of the AUDIT task. """ __tablename__ = "audit" shortname = "AUDIT" provides_trackers = True prohibits_commercial = True NQUESTIONS = 10 TASK_FIELDS = strseq("q", 1, NQUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("WHO Alcohol Use Disorders Identification Test") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="AUDIT total score", axis_label="Total score (out of 40)", axis_min=-0.5, axis_max=40.5, horizontal_lines=[7.5], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [CtvInfo(content=f"AUDIT total score {self.total_score()}/40")] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment="Total score (/40)", ) ] # noinspection PyUnresolvedReferences def is_complete(self) -> bool: if not self.field_contents_valid(): return False if self.q1 is None or self.q9 is None or self.q10 is None: return False if self.q1 == 0: # Special limited-information completeness return True if (self.q2 is not None and self.q3 is not None and (self.q2 + self.q3 == 0)): # Special limited-information completeness return True # Otherwise, any null values cause problems return self.all_fields_not_none(self.TASK_FIELDS) def total_score(self) -> int: return self.sum_fields(self.TASK_FIELDS) # noinspection PyUnresolvedReferences def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() exceeds_cutoff = score >= 8 q1_dict = {None: None} q2_dict = {None: None} q3_to_8_dict = {None: None} q9_to_10_dict = {None: None} for option in range(0, 5): q1_dict[option] = (str(option) + " – " + self.wxstring(req, "q1_option" + str(option))) q2_dict[option] = (str(option) + " – " + self.wxstring(req, "q2_option" + str(option))) q3_to_8_dict[option] = ( str(option) + " – " + self.wxstring(req, "q3to8_option" + str(option))) if option != 1 and option != 3: q9_to_10_dict[option] = ( str(option) + " – " + self.wxstring(req, "q9to10_option" + str(option))) q_a = tr_qa(self.wxstring(req, "q1_s"), get_from_dict(q1_dict, self.q1)) q_a += tr_qa(self.wxstring(req, "q2_s"), get_from_dict(q2_dict, self.q2)) for q in range(3, 8 + 1): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict(q3_to_8_dict, getattr(self, "q" + str(q))), ) q_a += tr_qa(self.wxstring(req, "q9_s"), get_from_dict(q9_to_10_dict, self.q9)) q_a += tr_qa(self.wxstring(req, "q10_s"), get_from_dict(q9_to_10_dict, self.q10)) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {tr(req.wsstring(SS.TOTAL_SCORE), answer(score) + " / 40")} {tr_qa(self.wxstring(req, "exceeds_standard_cutoff"), get_yes_no(req, exceeds_cutoff))} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.COPYRIGHT}"> AUDIT: Copyright © World Health Organization. Reproduced here under the permissions granted for NON-COMMERCIAL use only. You must obtain permission from the copyright holder for any other use. </div> """ def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.AUDIT_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.AUDIT_SCALE), {req.snomed(SnomedLookup.AUDIT_SCORE): self.total_score()}, )) return codes
class AuditC(TaskHasPatientMixin, Task, metaclass=AuditMetaclass): __tablename__ = "audit_c" shortname = "AUDIT-C" extrastring_taskname = "audit" # shares strings with AUDIT info_filename_stem = extrastring_taskname prohibits_commercial = True NQUESTIONS = 3 TASK_FIELDS = strseq("q", 1, NQUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("AUDIT Alcohol Consumption Questions") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="AUDIT-C total score", axis_label="Total score (out of 12)", axis_min=-0.5, axis_max=12.5, ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=f"AUDIT-C total score {self.total_score()}/12") ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment="Total score (/12)", ) ] def is_complete(self) -> bool: return self.all_fields_not_none(self.TASK_FIELDS) def total_score(self) -> int: return self.sum_fields(self.TASK_FIELDS) def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() q1_dict = {None: None} q2_dict = {None: None} q3_dict = {None: None} for option in range(0, 5): q1_dict[option] = (str(option) + " – " + self.wxstring(req, "q1_option" + str(option))) if option == 0: # special! q2_dict[option] = (str(option) + " – " + self.wxstring(req, "c_q2_option0")) else: q2_dict[option] = ( str(option) + " – " + self.wxstring(req, "q2_option" + str(option))) q3_dict[option] = ( str(option) + " – " + self.wxstring(req, "q3to8_option" + str(option))) # noinspection PyUnresolvedReferences return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {tr(req.sstring(SS.TOTAL_SCORE), answer(score) + " / 12")} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer</th> </tr> {tr_qa(self.wxstring(req, "c_q1_question"), get_from_dict(q1_dict, self.q1))} {tr_qa(self.wxstring(req, "c_q2_question"), get_from_dict(q2_dict, self.q2))} {tr_qa(self.wxstring(req, "c_q3_question"), get_from_dict(q3_dict, self.q3))} </table> <div class="{CssClass.COPYRIGHT}"> AUDIT: Copyright © World Health Organization. Reproduced here under the permissions granted for NON-COMMERCIAL use only. You must obtain permission from the copyright holder for any other use. AUDIT-C: presumed to have the same restrictions. </div> """ def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.AUDITC_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.AUDITC_SCALE), { req.snomed(SnomedLookup.AUDITC_SCORE): self.total_score() }, )) return codes
class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass): """ Server implementation of the CESD task. """ __tablename__ = 'cesd' shortname = 'CESD' longname = 'Center for Epidemiologic Studies Depression Scale' provides_trackers = True extrastring_taskname = "cesd" N_QUESTIONS = 20 N_ANSWERS = 4 DEPRESSION_RISK_THRESHOLD = 16 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS) TASK_FIELDS = SCORED_FIELDS MIN_SCORE = 0 MAX_SCORE = 3 * N_QUESTIONS REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16] # noinspection PyMethodParameters @classproperty def minimum_client_version(cls) -> Version: return Version("2.2.8") def is_complete(self) -> bool: return (self.are_all_fields_complete(self.TASK_FIELDS) and self.field_contents_valid()) def total_score(self) -> int: # Need to store values as per original then flip here total = 0 for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1): score = getattr(self, fieldname) if score is None: continue if qnum in self.REVERSE_SCORED_QUESTIONS: total += 3 - score else: total += score return total def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: line_step = 20 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 # noinspection PyTypeChecker return [ TrackerInfo( value=self.total_score(), plot_label="CESD total score", axis_label="Total score ({}-{})".format( self.MIN_SCORE, self.MAX_SCORE), axis_min=self.MIN_SCORE - 0.5, axis_max=self.MAX_SCORE + 0.5, axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE, self.MAX_SCORE, step=line_step), horizontal_lines=equally_spaced_int(self.MIN_SCORE + line_step, self.MAX_SCORE - line_step, step=line_step) + [threshold_line], horizontal_labels=[ TrackerLabel(threshold_line, self.wxstring(req, "depression_or_risk_of")), ]) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content="CESD total score {}".format(self.total_score())) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="depression_risk", coltype=Boolean(), value=self.has_depression_risk(), comment="Has depression or at risk of depression"), ] def has_depression_risk(self) -> bool: return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() answer_dict = {None: None} for option in range(self.N_ANSWERS): answer_dict[option] = str(option) + " – " + \ self.wxstring(req, "a" + str(option)) q_a = "" for q in range(1, self.N_QUESTIONS): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict(answer_dict, getattr(self, "q" + str(q)))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {depression_or_risk_of} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="70%">Question</th> <th width="30%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Presence of depression (or depression risk) is indicated by a score ≥ 16 </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr_qa( "{} (0–60)".format(req.wappstring("total_score")), score), depression_or_risk_of=tr_qa( self.wxstring(req, "depression_or_risk_of") + "? <sup>[1]</sup>", get_yes_no(req, self.has_depression_risk())), q_a=q_a, ) return h
class Cesdr(TaskHasPatientMixin, Task, metaclass=CesdrMetaclass): """ Server implementation of the CESD task. """ __tablename__ = 'cesdr' shortname = 'CESD-R' longname = 'Center for Epidemiologic Studies Depression Scale (Revised)' provides_trackers = True extrastring_taskname = "cesdr" CAT_NONCLINICAL = 0 CAT_SUB = 1 CAT_POSS_MAJOR = 2 CAT_PROB_MAJOR = 3 CAT_MAJOR = 4 DEPRESSION_RISK_THRESHOLD = 16 FREQ_NOT_AT_ALL = 0 FREQ_1_2_DAYS_LAST_WEEK = 1 FREQ_3_4_DAYS_LAST_WEEK = 2 FREQ_5_7_DAYS_LAST_WEEK = 3 FREQ_DAILY_2_WEEKS = 4 N_QUESTIONS = 20 N_ANSWERS = 5 POSS_MAJOR_THRESH = 2 PROB_MAJOR_THRESH = 3 MAJOR_THRESH = 4 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS) TASK_FIELDS = SCORED_FIELDS MIN_SCORE = 0 MAX_SCORE = 3 * N_QUESTIONS # noinspection PyMethodParameters @classproperty def minimum_client_version(cls) -> Version: return Version("2.2.8") def is_complete(self) -> bool: return (self.are_all_fields_complete(self.TASK_FIELDS) and self.field_contents_valid()) def total_score(self) -> int: return ( self.sum_fields(self.SCORED_FIELDS) - self.count_where(self.SCORED_FIELDS, [self.FREQ_DAILY_2_WEEKS])) def get_depression_category(self) -> int: if not self.has_depression_risk(): return self.CAT_SUB q_group_anhedonia = [8, 10] q_group_dysphoria = [2, 4, 6] other_q_groups = { 'appetite': [1, 18], 'sleep': [5, 11, 19], 'thinking': [3, 20], 'guilt': [9, 17], 'tired': [7, 16], 'movement': [12, 13], 'suicidal': [14, 15] } # Dysphoria or anhedonia must be present at frequency FREQ_DAILY_2_WEEKS anhedonia_criterion = ( self.fulfils_group_criteria(q_group_anhedonia, True) or self.fulfils_group_criteria(q_group_dysphoria, True)) if anhedonia_criterion: category_count_high_freq = 0 category_count_lower_freq = 0 for qgroup in other_q_groups.values(): if self.fulfils_group_criteria(qgroup, True): # Category contains an answer == FREQ_DAILY_2_WEEKS category_count_high_freq += 1 if self.fulfils_group_criteria(qgroup, False): # Category contains an answer == FREQ_DAILY_2_WEEKS or # FREQ_5_7_DAYS_LAST_WEEK category_count_lower_freq += 1 if category_count_high_freq >= self.MAJOR_THRESH: # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) # plus 4 other symptom groups at FREQ_DAILY_2_WEEKS return self.CAT_MAJOR if category_count_lower_freq >= self.PROB_MAJOR_THRESH: # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) # plus 3 other symptom groups at FREQ_DAILY_2_WEEKS or # FREQ_5_7_DAYS_LAST_WEEK return self.CAT_PROB_MAJOR if category_count_lower_freq >= self.POSS_MAJOR_THRESH: # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) # plus 2 other symptom groups at FREQ_DAILY_2_WEEKS or # FREQ_5_7_DAYS_LAST_WEEK return self.CAT_POSS_MAJOR if self.has_depression_risk(): # Total CESD-style score >= 16 but doesn't meet other criteria. return self.CAT_SUB return self.CAT_NONCLINICAL def fulfils_group_criteria(self, qnums: List[int], nearly_every_day_2w: bool) -> bool: qstrings = ["q" + str(qnum) for qnum in qnums] if nearly_every_day_2w: possible_values = [self.FREQ_DAILY_2_WEEKS] else: possible_values = [ self.FREQ_5_7_DAYS_LAST_WEEK, self.FREQ_DAILY_2_WEEKS ] count = self.count_where(qstrings, possible_values) return count > 0 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: line_step = 20 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 # noinspection PyTypeChecker return [ TrackerInfo( value=self.total_score(), plot_label="CESD-R total score", axis_label="Total score ({}-{})".format( self.MIN_SCORE, self.MAX_SCORE), axis_min=self.MIN_SCORE - 0.5, axis_max=self.MAX_SCORE + 0.5, axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE, self.MAX_SCORE, step=line_step), horizontal_lines=equally_spaced_int(self.MIN_SCORE + line_step, self.MAX_SCORE - line_step, step=line_step) + [threshold_line], horizontal_labels=[ TrackerLabel(threshold_line, self.wxstring(req, "depression_or_risk_of")), ]) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content="CESD-R total score {}".format(self.total_score())) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="depression_risk", coltype=Boolean(), value=self.has_depression_risk(), comment="Has depression or at risk of depression"), ] def has_depression_risk(self) -> bool: return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() answer_dict = {None: None} for option in range(self.N_ANSWERS): answer_dict[option] = str(option) + " – " + \ self.wxstring(req, "a" + str(option)) q_a = "" for q in range(1, self.N_QUESTIONS): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict(answer_dict, getattr(self, "q" + str(q)))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {depression_or_risk_of} {provisional_diagnosis} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="70%">Question</th> <th width="30%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Presence of depression (or depression risk) is indicated by a score ≥ 16 [2] Diagnostic criteria described at <a href="https://cesd-r.com/cesdr/">https://cesd-r.com/cesdr/</a> </div> """.format( # noqa CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr_qa( "{} (0–60)".format(req.wappstring("total_score")), score), depression_or_risk_of=tr_qa( self.wxstring(req, "depression_or_risk_of") + "? <sup>[1]</sup>", get_yes_no(req, self.has_depression_risk())), provisional_diagnosis=tr( 'Provisional diagnosis <sup>[2]</sup>', self.wxstring( req, "category_" + str(self.get_depression_category()))), q_a=q_a, ) return h
class Ybocs(TaskHasClinicianMixin, TaskHasPatientMixin, Task, metaclass=YbocsMetaclass): """ Server implementation of the Y-BOCS task. """ __tablename__ = "ybocs" shortname = "Y-BOCS" provides_trackers = True NTARGETS = 3 QINFO = [ # number, max score, minimal comment ("1", 4, "obsessions: time"), ("1b", 4, "obsessions: obsession-free interval"), ("2", 4, "obsessions: interference"), ("3", 4, "obsessions: distress"), ("4", 4, "obsessions: resistance"), ("5", 4, "obsessions: control"), ("6", 4, "compulsions: time"), ("6b", 4, "compulsions: compulsion-free interval"), ("7", 4, "compulsions: interference"), ("8", 4, "compulsions: distress"), ("9", 4, "compulsions: resistance"), ("10", 4, "compulsions: control"), ("11", 4, "insight"), ("12", 4, "avoidance"), ("13", 4, "indecisiveness"), ("14", 4, "overvalued responsibility"), ("15", 4, "slowness"), ("16", 4, "doubting"), ("17", 6, "global severity"), ("18", 6, "global improvement"), ("19", 3, "reliability"), ] QUESTION_FIELDS = ["q" + x[0] for x in QINFO] SCORED_QUESTIONS = strseq("q", 1, 10) OBSESSION_QUESTIONS = strseq("q", 1, 5) COMPULSION_QUESTIONS = strseq("q", 6, 10) MAX_TOTAL = 40 MAX_OBS = 20 MAX_COM = 20 @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Yale–Brown Obsessive Compulsive Scale") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="Y-BOCS total score (lower is better)", axis_label=f"Total score (out of {self.MAX_TOTAL})", axis_min=-0.5, axis_max=self.MAX_TOTAL + 0.5, ), TrackerInfo( value=self.obsession_score(), plot_label="Y-BOCS obsession score (lower is better)", axis_label=f"Total score (out of {self.MAX_OBS})", axis_min=-0.5, axis_max=self.MAX_OBS + 0.5, ), TrackerInfo( value=self.compulsion_score(), plot_label="Y-BOCS compulsion score (lower is better)", axis_label=f"Total score (out of {self.MAX_COM})", axis_min=-0.5, axis_max=self.MAX_COM + 0.5, ), ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment=f"Total score (/ {self.MAX_TOTAL})", ), SummaryElement( name="obsession_score", coltype=Integer(), value=self.obsession_score(), comment=f"Obsession score (/ {self.MAX_OBS})", ), SummaryElement( name="compulsion_score", coltype=Integer(), value=self.compulsion_score(), comment=f"Compulsion score (/ {self.MAX_COM})", ), ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE t = self.total_score() o = self.obsession_score() c = self.compulsion_score() return [ CtvInfo( content=("Y-BOCS total score {t}/{mt} (obsession {o}/{mo}, " "compulsion {c}/{mc})".format( t=t, mt=self.MAX_TOTAL, o=o, mo=self.MAX_OBS, c=c, mc=self.MAX_COM, ))) ] def total_score(self) -> int: return self.sum_fields(self.SCORED_QUESTIONS) def obsession_score(self) -> int: return self.sum_fields(self.OBSESSION_QUESTIONS) def compulsion_score(self) -> int: return self.sum_fields(self.COMPULSION_QUESTIONS) def is_complete(self) -> bool: return self.field_contents_valid() and self.all_fields_not_none( self.SCORED_QUESTIONS) def get_task_html(self, req: CamcopsRequest) -> str: target_symptoms = "" for col in self.TARGET_COLUMNS: target_symptoms += tr(col.comment, answer(getattr(self, col.name))) q_a = "" for qi in self.QINFO: fieldname = "q" + qi[0] value = getattr(self, fieldname) q_a += tr( self.wxstring(req, fieldname + "_title"), answer( self.wxstring(req, fieldname + "_a" + str(value), value ) if value is not None else None), ) return f"""
class Ciwa(TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=CiwaMetaclass): """ Server implementation of the CIWA-Ar task. """ __tablename__ = "ciwa" shortname = "CIWA-Ar" provides_trackers = True NSCOREDQUESTIONS = 10 SCORED_QUESTIONS = strseq("q", 1, NSCOREDQUESTIONS) q10 = CamcopsColumn( "q10", Integer, permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4), comment="Q10, orientation/clouding of sensorium (0-4, higher worse)", ) t = Column("t", Float, comment="Temperature (degrees C)") hr = CamcopsColumn( "hr", Integer, permitted_value_checker=MIN_ZERO_CHECKER, comment="Heart rate (beats/minute)", ) sbp = CamcopsColumn( "sbp", Integer, permitted_value_checker=MIN_ZERO_CHECKER, comment="Systolic blood pressure (mmHg)", ) dbp = CamcopsColumn( "dbp", Integer, permitted_value_checker=MIN_ZERO_CHECKER, comment="Diastolic blood pressure (mmHg)", ) rr = CamcopsColumn( "rr", Integer, permitted_value_checker=MIN_ZERO_CHECKER, comment="Respiratory rate (breaths/minute)", ) MAX_SCORE = 67 @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Clinical Institute Withdrawal Assessment for Alcohol " "Scale, Revised") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="CIWA total score", axis_label=f"Total score (out of {self.MAX_SCORE})", axis_min=-0.5, axis_max=self.MAX_SCORE + 0.5, horizontal_lines=[14.5, 7.5], horizontal_labels=[ TrackerLabel(17, req.sstring(SS.SEVERE)), TrackerLabel(11, req.sstring(SS.MODERATE)), TrackerLabel(3.75, req.sstring(SS.MILD)), ], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=f"CIWA total score: " f"{self.total_score()}/{self.MAX_SCORE}") ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score (/{self.MAX_SCORE})", ), SummaryElement( name="severity", coltype=SummaryCategoryColType, value=self.severity(req), comment="Likely severity", ), ] def is_complete(self) -> bool: return (self.all_fields_not_none(self.SCORED_QUESTIONS) and self.field_contents_valid()) def total_score(self) -> int: return self.sum_fields(self.SCORED_QUESTIONS) def severity(self, req: CamcopsRequest) -> str: score = self.total_score() if score >= 15: severity = self.wxstring(req, "category_severe") elif score >= 8: severity = self.wxstring(req, "category_moderate") else: severity = self.wxstring(req, "category_mild") return severity def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() severity = self.severity(req) answer_dicts_dict = {} for q in self.SCORED_QUESTIONS: d = {None: None} for option in range(0, 8): if option > 4 and q == "q10": continue d[option] = self.wxstring(req, q + "_option" + str(option)) answer_dicts_dict[q] = d q_a = "" for q in range(1, Ciwa.NSCOREDQUESTIONS + 1): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict( answer_dicts_dict["q" + str(q)], getattr(self, "q" + str(q)), ), ) tr_total_score = tr(req.sstring(SS.TOTAL_SCORE), answer(score) + f" / {self.MAX_SCORE}") tr_severity = tr_qa( self.wxstring(req, "severity") + " <sup>[1]</sup>", severity) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {tr_total_score} {tr_severity} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="35%">Question</th> <th width="65%">Answer</th> </tr> {q_a} {subheading_spanning_two_columns( self.wxstring(req, "vitals_title"))} {tr_qa(self.wxstring(req, "t"), self.t)} {tr_qa(self.wxstring(req, "hr"), self.hr)} {tr(self.wxstring(req, "bp"), answer(self.sbp) + " / " + answer(self.dbp))} {tr_qa(self.wxstring(req, "rr"), self.rr)} </table> <div class="{CssClass.FOOTNOTES}"> [1] Total score ≥15 severe, ≥8 moderate, otherwise mild/minimal. </div> """ def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.CIWA_AR_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.CIWA_AR_SCALE), { req.snomed(SnomedLookup.CIWA_AR_SCORE): self.total_score() }, )) return codes
class Pswq(TaskHasPatientMixin, Task, metaclass=PswqMetaclass): """ Server implementation of the PSWQ task. """ __tablename__ = "pswq" shortname = "PSWQ" provides_trackers = True MIN_PER_Q = 1 MAX_PER_Q = 5 NQUESTIONS = 16 REVERSE_SCORE = [1, 3, 8, 10, 11] TASK_FIELDS = strseq("q", 1, NQUESTIONS) MIN_TOTAL = MIN_PER_Q * NQUESTIONS MAX_TOTAL = MAX_PER_Q * NQUESTIONS @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Penn State Worry Questionnaire") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="PSWQ total score (lower is better)", axis_label=f"Total score ({self.MIN_TOTAL}–{self.MAX_TOTAL})", axis_min=self.MIN_TOTAL - 0.5, axis_max=self.MAX_TOTAL + 0.5, ) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment="Total score (16-80)", ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=f"PSWQ total score {self.total_score()} " f"(range {self.MIN_TOTAL}–{self.MAX_TOTAL})") ] def score(self, q: int) -> Optional[int]: value = getattr(self, "q" + str(q)) if value is None: return None if q in self.REVERSE_SCORE: return self.MAX_PER_Q + 1 - value else: return value def total_score(self) -> int: values = [self.score(q) for q in range(1, self.NQUESTIONS + 1)] return sum(v for v in values if v is not None) def is_complete(self) -> bool: return (self.all_fields_not_none(self.TASK_FIELDS) and self.field_contents_valid()) def get_task_html(self, req: CamcopsRequest) -> str: h = f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} <tr> <td>Total score (16–80)</td> <td>{answer(self.total_score())}</td> </td> </table> </div> <div class="{CssClass.EXPLANATION}"> Anchor points are 1 = {self.wxstring(req, "anchor1")}, 5 = {self.wxstring(req, "anchor5")}. Questions {", ".join(str(x) for x in self.REVERSE_SCORE)} are reverse-scored. </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="70%">Question</th> <th width="15%">Answer (1–5)</th> <th width="15%">Score (1–5)</th> </tr> """ for q in range(1, self.NQUESTIONS + 1): a = getattr(self, "q" + str(q)) score = self.score(q) h += tr(self.wxstring(req, "q" + str(q)), answer(a), score) h += """ </table> """ return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression(req.snomed( SnomedLookup.PSWQ_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.PSWQ_SCALE), {req.snomed(SnomedLookup.PSWQ_SCORE): self.total_score()}, )) return codes
class Rand36(TaskHasPatientMixin, Task, metaclass=Rand36Metaclass): """ Server implementation of the RAND-36 task. """ __tablename__ = "rand36" shortname = "RAND-36" longname = "RAND 36-Item Short Form Health Survey 1.0" provides_trackers = True NQUESTIONS = 36 q1 = CamcopsColumn("q1", Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment="Q1 (general health) (1 excellent - 5 poor)") q2 = CamcopsColumn( "q2", Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment="Q2 (health cf. 1y ago) (1 much better - 5 much worse)") q20 = CamcopsColumn( "q20", Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment="Q20 (past 4 weeks, to what extent physical health/" "emotional problems interfered with social activity) " "(1 not at all - 5 extremely)") q21 = CamcopsColumn( "q21", Integer, permitted_value_checker=ONE_TO_SIX_CHECKER, comment="Q21 (past 4 weeks, how much pain (1 none - 6 very severe)") q22 = CamcopsColumn( "q22", Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment="Q22 (past 4 weeks, pain interfered with normal activity " "(1 not at all - 5 extremely)") q32 = CamcopsColumn( "q32", Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment="Q32 (past 4 weeks, how much of the time has physical " "health/emotional problems interfered with social activities " "(1 all of the time - 5 none of the time)") # ... note Q32 extremely similar to Q20. TASK_FIELDS = strseq("q", 1, NQUESTIONS) def is_complete(self) -> bool: return (self.are_all_fields_complete(self.TASK_FIELDS) and self.field_contents_valid()) @classmethod def tracker_element(cls, value: float, plot_label: str) -> TrackerInfo: return TrackerInfo(value=value, plot_label="RAND-36: " + plot_label, axis_label="Scale score (out of 100)", axis_min=-0.5, axis_max=100.5) def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ self.tracker_element(self.score_overall(), self.wxstring(req, "score_overall")), self.tracker_element( self.score_physical_functioning(), self.wxstring(req, "score_physical_functioning")), self.tracker_element( self.score_role_limitations_physical(), self.wxstring(req, "score_role_limitations_physical")), self.tracker_element( self.score_role_limitations_emotional(), self.wxstring(req, "score_role_limitations_emotional")), self.tracker_element(self.score_energy(), self.wxstring(req, "score_energy")), self.tracker_element( self.score_emotional_wellbeing(), self.wxstring(req, "score_emotional_wellbeing")), self.tracker_element( self.score_social_functioning(), self.wxstring(req, "score_social_functioning")), self.tracker_element(self.score_pain(), self.wxstring(req, "score_pain")), self.tracker_element(self.score_general_health(), self.wxstring(req, "score_general_health")), ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=( "RAND-36 (scores out of 100, 100 best): overall {ov}, " "physical functioning {pf}, physical role " "limitations {prl}, emotional role limitations {erl}, " "energy {e}, emotional wellbeing {ew}, social " "functioning {sf}, pain {p}, general health {gh}.".format( ov=self.score_overall(), pf=self.score_physical_functioning(), prl=self.score_role_limitations_physical(), erl=self.score_role_limitations_emotional(), e=self.score_energy(), ew=self.score_emotional_wellbeing(), sf=self.score_social_functioning(), p=self.score_pain(), gh=self.score_general_health(), ))) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="overall", coltype=Float(), value=self.score_overall(), comment="Overall mean score (0-100, higher better)"), SummaryElement( name="physical_functioning", coltype=Float(), value=self.score_physical_functioning(), comment="Physical functioning score (0-100, higher better)"), SummaryElement( name="role_limitations_physical", coltype=Float(), value=self.score_role_limitations_physical(), comment="Role limitations due to physical health score " "(0-100, higher better)"), SummaryElement( name="role_limitations_emotional", coltype=Float(), value=self.score_role_limitations_emotional(), comment="Role limitations due to emotional problems score " "(0-100, higher better)"), SummaryElement( name="energy", coltype=Float(), value=self.score_energy(), comment="Energy/fatigue score (0-100, higher better)"), SummaryElement( name="emotional_wellbeing", coltype=Float(), value=self.score_emotional_wellbeing(), comment="Emotional well-being score (0-100, higher better)"), SummaryElement( name="social_functioning", coltype=Float(), value=self.score_social_functioning(), comment="Social functioning score (0-100, higher better)"), SummaryElement(name="pain", coltype=Float(), value=self.score_pain(), comment="Pain score (0-100, higher better)"), SummaryElement( name="general_health", coltype=Float(), value=self.score_general_health(), comment="General health score (0-100, higher better)"), ] # Scoring def recode(self, q: int) -> Optional[float]: x = getattr(self, "q" + str(q)) # response if x is None or x < 1: return None # http://m.rand.org/content/dam/rand/www/external/health/ # surveys_tools/mos/mos_core_36item_scoring.pdf if q == 1 or q == 2 or q == 20 or q == 22 or q == 34 or q == 36: # 1 becomes 100, 2 => 75, 3 => 50, 4 =>25, 5 => 0 if x > 5: return None return 100 - 25 * (x - 1) elif 3 <= q <= 12: # 1 => 0, 2 => 50, 3 => 100 if x > 3: return None return 50 * (x - 1) elif 13 <= q <= 19: # 1 => 0, 2 => 100 if x > 2: return None return 100 * (x - 1) elif q == 21 or q == 23 or q == 26 or q == 27 or q == 30: # 1 => 100, 2 => 80, 3 => 60, 4 => 40, 5 => 20, 6 => 0 if x > 6: return None return 100 - 20 * (x - 1) elif q == 24 or q == 25 or q == 28 or q == 29 or q == 31: # 1 => 0, 2 => 20, 3 => 40, 4 => 60, 5 => 80, 6 => 100 if x > 6: return None return 20 * (x - 1) elif q == 32 or q == 33 or q == 35: # 1 => 0, 2 => 25, 3 => 50, 4 => 75, 5 => 100 if x > 5: return None return 25 * (x - 1) return None def score_physical_functioning(self) -> Optional[float]: return mean([ self.recode(3), self.recode(4), self.recode(5), self.recode(6), self.recode(7), self.recode(8), self.recode(9), self.recode(10), self.recode(11), self.recode(12) ]) def score_role_limitations_physical(self) -> Optional[float]: return mean([ self.recode(13), self.recode(14), self.recode(15), self.recode(16) ]) def score_role_limitations_emotional(self) -> Optional[float]: return mean([self.recode(17), self.recode(18), self.recode(19)]) def score_energy(self) -> Optional[float]: return mean([ self.recode(23), self.recode(27), self.recode(29), self.recode(31) ]) def score_emotional_wellbeing(self) -> Optional[float]: return mean([ self.recode(24), self.recode(25), self.recode(26), self.recode(28), self.recode(30) ]) def score_social_functioning(self) -> Optional[float]: return mean([self.recode(20), self.recode(32)]) def score_pain(self) -> Optional[float]: return mean([self.recode(21), self.recode(22)]) def score_general_health(self) -> Optional[float]: return mean([ self.recode(1), self.recode(33), self.recode(34), self.recode(35), self.recode(36) ]) @staticmethod def format_float_for_display(val: Optional[float]) -> Optional[str]: if val is None: return None return "{:.1f}".format(val) def score_overall(self) -> Optional[float]: values = [] for q in range(1, self.NQUESTIONS + 1): values.append(self.recode(q)) return mean(values) @staticmethod def section_row_html(text: str) -> str: return tr_span_col(text, cols=3, tr_class=CssClass.SUBHEADING) def answer_text(self, req: CamcopsRequest, q: int, v: Any) -> Optional[str]: if v is None: return None # wxstring has its own validity checking, so we can do: if q == 1 or q == 2 or (20 <= q <= 22) or q == 32: return self.wxstring(req, "q" + str(q) + "_option" + str(v)) elif 3 <= q <= 12: return self.wxstring(req, "activities_option" + str(v)) elif 13 <= q <= 19: return self.wxstring(req, "yesno_option" + str(v)) elif 23 <= q <= 31: return self.wxstring(req, "last4weeks_option" + str(v)) elif 33 <= q <= 36: return self.wxstring(req, "q33to36_option" + str(v)) else: return None def answer_row_html(self, req: CamcopsRequest, q: int) -> str: qtext = self.wxstring(req, "q" + str(q)) v = getattr(self, "q" + str(q)) atext = self.answer_text(req, q, v) s = self.recode(q) return tr(qtext, answer(v) + ": " + answer(atext), answer(s, formatter_answer=identity)) @staticmethod def scoreline(text: str, footnote_num: int, score: Optional[float]) -> str: return tr(text + " <sup>[{}]</sup>".format(footnote_num), answer(score) + " / 100") def get_task_html(self, req: CamcopsRequest) -> str: h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), ) h += self.scoreline( self.wxstring(req, "score_overall"), 1, self.format_float_for_display(self.score_overall())) h += self.scoreline( self.wxstring(req, "score_physical_functioning"), 2, self.format_float_for_display(self.score_physical_functioning())) h += self.scoreline( self.wxstring(req, "score_role_limitations_physical"), 3, self.format_float_for_display( self.score_role_limitations_physical())) h += self.scoreline( self.wxstring(req, "score_role_limitations_emotional"), 4, self.format_float_for_display( self.score_role_limitations_emotional())) h += self.scoreline(self.wxstring(req, "score_energy"), 5, self.format_float_for_display(self.score_energy())) h += self.scoreline( self.wxstring(req, "score_emotional_wellbeing"), 6, self.format_float_for_display(self.score_emotional_wellbeing())) h += self.scoreline( self.wxstring(req, "score_social_functioning"), 7, self.format_float_for_display(self.score_social_functioning())) h += self.scoreline(self.wxstring(req, "score_pain"), 8, self.format_float_for_display(self.score_pain())) h += self.scoreline( self.wxstring(req, "score_general_health"), 9, self.format_float_for_display(self.score_general_health())) h += """ </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="30%">Answer</th> <th width="10%">Score</th> </tr> """.format(CssClass=CssClass, ) for q in range(1, 2 + 1): h += self.answer_row_html(req, q) h += self.section_row_html(self.wxstring(req, "activities_q")) for q in range(3, 12 + 1): h += self.answer_row_html(req, q) h += self.section_row_html( self.wxstring(req, "work_activities_physical_q")) for q in range(13, 16 + 1): h += self.answer_row_html(req, q) h += self.section_row_html( self.wxstring(req, "work_activities_emotional_q")) for q in range(17, 19 + 1): h += self.answer_row_html(req, q) h += self.section_row_html("<br>") h += self.answer_row_html(req, 20) h += self.section_row_html("<br>") for q in range(21, 22 + 1): h += self.answer_row_html(req, q) h += self.section_row_html( self.wxstring(req, "last4weeks_q_a") + " " + self.wxstring(req, "last4weeks_q_b")) for q in range(23, 31 + 1): h += self.answer_row_html(req, q) h += self.section_row_html("<br>") for q in [32]: h += self.answer_row_html(req, q) h += self.section_row_html(self.wxstring(req, "q33to36stem")) for q in range(33, 36 + 1): h += self.answer_row_html(req, q) h += """ </table> <div class="{CssClass.COPYRIGHT}"> The RAND 36-Item Short Form Health Survey was developed at RAND as part of the Medical Outcomes Study. See http://www.rand.org/health/surveys_tools/mos/mos_core_36item.html </div> <div class="{CssClass.FOOTNOTES}"> All questions are first transformed to a score in the range 0–100. Higher scores are always better. Then: [1] Mean of all 36 questions. [2] Mean of Q3–12 inclusive. [3] Q13–16. [4] Q17–19. [5] Q23, 27, 29, 31. [6] Q24, 25, 26, 28, 30. [7] Q20, 32. [8] Q21, 22. [9] Q1, 33–36. </div> """.format(CssClass=CssClass, ) return h
class Sfmpq2(TaskHasPatientMixin, Task, metaclass=Sfmpq2Metaclass): __tablename__ = "sfmpq2" shortname = "SF-MPQ2" N_QUESTIONS = 22 MAX_SCORE_PER_Q = 10 ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) CONTINUOUS_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {1, 5, 6, 8, 9, 10}) INTERMITTENT_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {2, 3, 4, 11, 16, 18}) NEUROPATHIC_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {7, 17, 19, 20, 21, 22}) AFFECTIVE_PAIN_QUESTIONS = Task.fieldnames_from_list("q", {12, 13, 14, 15}) @staticmethod def longname(req: CamcopsRequest) -> str: _ = req.gettext return _("Short-Form McGill Pain Questionnaire 2") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_pain", coltype=Float(), value=self.total_pain(), comment=f"Total pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="continuous_pain", coltype=Float(), value=self.continuous_pain(), comment=f"Continuous pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="intermittent_pain", coltype=Float(), value=self.intermittent_pain(), comment=f"Intermittent pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="neuropathic_pain", coltype=Float(), value=self.neuropathic_pain(), comment=f"Neuropathic pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="affective_pain", coltype=Float(), value=self.affective_pain(), comment=f"Affective pain (/{self.MAX_SCORE_PER_Q})", ), ] def is_complete(self) -> bool: if self.any_fields_none(self.ALL_QUESTIONS): return False if not self.field_contents_valid(): return False return True def total_pain(self) -> float: return self.mean_fields(self.ALL_QUESTIONS) def continuous_pain(self) -> float: return self.mean_fields(self.CONTINUOUS_PAIN_QUESTIONS) def intermittent_pain(self) -> float: return self.mean_fields(self.INTERMITTENT_PAIN_QUESTIONS) def neuropathic_pain(self) -> float: return self.mean_fields(self.NEUROPATHIC_PAIN_QUESTIONS) def affective_pain(self) -> float: return self.mean_fields(self.AFFECTIVE_PAIN_QUESTIONS) def format_average(self, value) -> str: return "{} / {}".format( answer(ws.number_to_dp(value, 3, default="?")), self.MAX_SCORE_PER_Q, ) def get_task_html(self, req: CamcopsRequest) -> str: rows = "" for q_num in range(1, self.N_QUESTIONS + 1): q_field = "q" + str(q_num) question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) score = getattr(self, q_field) rows += tr_qa(question_cell, score) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_pain} {continuous_pain} {intermittent_pain} {neuropathic_pain} {affective_pain} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer <sup>[6]</sup></th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Average of items 1–22. [2] Average of items 1, 5, 6, 8, 9, 10. [3] Average of items 2, 3, 4, 11, 16, 18. [4] Average of items 7, 17, 19, 20, 21, 22. [5] Average of items 12, 13, 14, 15. [6] All items are rated from “0 – none” to “10 – worst possible”. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_pain=tr( self.wxstring(req, "total_pain") + " <sup>[1]</sup>", self.format_average(self.total_pain()), ), continuous_pain=tr( self.wxstring(req, "continuous_pain") + " <sup>[2]</sup>", self.format_average(self.continuous_pain()), ), intermittent_pain=tr( self.wxstring(req, "intermittent_pain") + " <sup>[3]</sup>", self.format_average(self.intermittent_pain()), ), neuropathic_pain=tr( self.wxstring(req, "neuropathic_pain") + " <sup>[4]</sup>", self.format_average(self.neuropathic_pain()), ), affective_pain=tr( self.wxstring(req, "affective_pain") + " <sup>[5]</sup>", self.format_average(self.affective_pain()), ), rows=rows, ) return html
class Hamd7(TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=Hamd7Metaclass): """ Server implementation of the HAMD-7 task. """ __tablename__ = "hamd7" shortname = "HAMD-7" info_filename_stem = "hamd" provides_trackers = True NQUESTIONS = 7 TASK_FIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = 26 @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Hamilton Rating Scale for Depression (7-item scale)") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="HAM-D-7 total score", axis_label=f"Total score (out of {self.MAX_SCORE})", axis_min=-0.5, axis_max=self.MAX_SCORE + 0.5, horizontal_lines=[19.5, 11.5, 3.5], horizontal_labels=[ TrackerLabel(23, self.wxstring(req, "severity_severe")), TrackerLabel(15.5, self.wxstring(req, "severity_moderate")), TrackerLabel(7.5, self.wxstring(req, "severity_mild")), TrackerLabel(1.75, self.wxstring(req, "severity_none")), ], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=(f"HAM-D-7 total score " f"{self.total_score()}/{self.MAX_SCORE} " f"({self.severity(req)})")) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score (/{self.MAX_SCORE})", ), SummaryElement( name="severity", coltype=SummaryCategoryColType, value=self.severity(req), comment="Severity", ), ] def is_complete(self) -> bool: return (self.all_fields_not_none(self.TASK_FIELDS) and self.field_contents_valid()) def total_score(self) -> int: return self.sum_fields(self.TASK_FIELDS) def severity(self, req: CamcopsRequest) -> str: score = self.total_score() if score >= 20: return self.wxstring(req, "severity_severe") elif score >= 12: return self.wxstring(req, "severity_moderate") elif score >= 4: return self.wxstring(req, "severity_mild") else: return self.wxstring(req, "severity_none") def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() severity = self.severity(req) answer_dicts = [] for q in range(1, self.NQUESTIONS + 1): d = {None: None} for option in range(0, 5): if q == 6 and option > 2: continue d[option] = self.wxstring( req, "q" + str(q) + "_option" + str(option)) answer_dicts.append(d) q_a = "" for q in range(1, self.NQUESTIONS + 1): q_a += tr_qa( self.wxstring(req, "q" + str(q) + "_s"), get_from_dict(answer_dicts[q - 1], getattr(self, "q" + str(q))), ) return """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {severity} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="30%">Question</th> <th width="70%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] ≥20 severe, ≥12 moderate, ≥4 mild, <4 none. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE), answer(score) + " / {}".format(self.MAX_SCORE), ), severity=tr_qa( self.wxstring(req, "severity") + " <sup>[1]</sup>", severity), q_a=q_a, )
class Mfi20(TaskHasPatientMixin, Task, metaclass=Mfi20Metaclass): __tablename__ = "mfi20" shortname = "MFI-20" prohibits_clinical = True prohibits_commercial = True N_QUESTIONS = 20 MIN_SCORE_PER_Q = 1 MAX_SCORE_PER_Q = 5 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS N_Q_PER_SUBSCALE = 4 # always MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) REVERSE_QUESTIONS = Task.fieldnames_from_list( "q", {2, 5, 9, 10, 13, 14, 16, 17, 18, 19} ) GENERAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {1, 5, 12, 16}) PHYSICAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {2, 8, 14, 20}) REDUCED_ACTIVITY_QUESTIONS = Task.fieldnames_from_list("q", {3, 6, 10, 17}) REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list( "q", {4, 9, 15, 18} ) MENTAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {7, 11, 13, 19}) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Multidimensional Fatigue Inventory") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]", ), SummaryElement( name="general_fatigue", coltype=Integer(), value=self.general_fatigue_score(), comment=f"General fatigue {subscale_range}", ), SummaryElement( name="physical_fatigue", coltype=Integer(), value=self.physical_fatigue_score(), comment=f"Physical fatigue {subscale_range}", ), SummaryElement( name="reduced_activity", coltype=Integer(), value=self.reduced_activity_score(), comment=f"Reduced activity {subscale_range}", ), SummaryElement( name="reduced_motivation", coltype=Integer(), value=self.reduced_motivation_score(), comment=f"Reduced motivation {subscale_range}", ), SummaryElement( name="mental_fatigue", coltype=Integer(), value=self.mental_fatigue_score(), comment=f"Mental fatigue {subscale_range}", ), ] def is_complete(self) -> bool: if self.any_fields_none(self.ALL_QUESTIONS): return False if not self.field_contents_valid(): return False return True def score_fields(self, fields: List[str]) -> int: total = 0 for f in fields: value = getattr(self, f) if value is not None: if f in self.REVERSE_QUESTIONS: value = self.MAX_SCORE_PER_Q + 1 - value total += value if value is not None else 0 return total def total_score(self) -> int: return self.score_fields(self.ALL_QUESTIONS) def general_fatigue_score(self) -> int: return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS) def physical_fatigue_score(self) -> int: return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS) def reduced_activity_score(self) -> int: return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS) def reduced_motivation_score(self) -> int: return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS) def mental_fatigue_score(self) -> int: return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS) def get_task_html(self, req: CamcopsRequest) -> str: fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]" subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" rows = "" for q_num in range(1, self.N_QUESTIONS + 1): q_field = "q" + str(q_num) question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) score = getattr(self, q_field) rows += tr_qa(question_cell, score) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {general_fatigue_score} {physical_fatigue_score} {reduced_activity_score} {reduced_motivation_score} {mental_fatigue_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer <sup>[8]</sup></th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Questions 2, 5, 9, 10, 13, 14, 16, 17, 18, 19 reverse-scored when summing. [2] Sum for questions 1–20. [3] General fatigue: Sum for questions 1, 5, 12, 16. [4] Physical fatigue: Sum for questions 2, 8, 14, 20. [5] Reduced activity: Sum for questions 3, 6, 10, 17. [6] Reduced motivation: Sum for questions 4, 9, 15, 18. [7] Mental fatigue: Sum for questions 7, 11, 13, 19. [8] All questions are rated from “1 – yes, that is true” to “5 – no, that is not true”. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE) + " <sup>[1][2]</sup>", f"{answer(self.total_score())} {fullscale_range}", ), general_fatigue_score=tr( self.wxstring(req, "general_fatigue") + " <sup>[1][3]</sup>", f"{answer(self.general_fatigue_score())} {subscale_range}", ), physical_fatigue_score=tr( self.wxstring(req, "physical_fatigue") + " <sup>[1][4]</sup>", f"{answer(self.physical_fatigue_score())} {subscale_range}", ), reduced_activity_score=tr( self.wxstring(req, "reduced_activity") + " <sup>[1][5]</sup>", f"{answer(self.reduced_activity_score())} {subscale_range}", ), reduced_motivation_score=tr( self.wxstring(req, "reduced_motivation") + " <sup>[1][6]</sup>", f"{answer(self.reduced_motivation_score())} {subscale_range}", ), mental_fatigue_score=tr( self.wxstring(req, "mental_fatigue") + " <sup>[1][7]</sup>", f"{answer(self.mental_fatigue_score())} {subscale_range}", ), rows=rows, ) return html
class Caps(TaskHasPatientMixin, Task, metaclass=CapsMetaclass): """ Server implementation of the CAPS task. """ __tablename__ = "caps" shortname = "CAPS" longname = "Cardiff Anomalous Perceptions Scale" provides_trackers = True NQUESTIONS = 32 ENDORSE_FIELDS = strseq("endorse", 1, NQUESTIONS) def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo(value=self.total_score(), plot_label="CAPS total score", axis_label="Total score (out of 32)", axis_min=-0.5, axis_max=32.5) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="total", coltype=Integer(), value=self.total_score(), comment="Total score (/32)"), SummaryElement(name="distress", coltype=Integer(), value=self.distress_score(), comment="Distress score (/160)"), SummaryElement(name="intrusiveness", coltype=Integer(), value=self.intrusiveness_score(), comment="Intrusiveness score (/160)"), SummaryElement(name="frequency", coltype=Integer(), value=self.frequency_score(), comment="Frequency score (/160)"), ] def is_question_complete(self, q: int) -> bool: if getattr(self, "endorse" + str(q)) is None: return False if getattr(self, "endorse" + str(q)): if getattr(self, "distress" + str(q)) is None: return False if getattr(self, "intrusiveness" + str(q)) is None: return False if getattr(self, "frequency" + str(q)) is None: return False return True def is_complete(self) -> bool: if not self.field_contents_valid(): return False for i in range(1, Caps.NQUESTIONS + 1): if not self.is_question_complete(i): return False return True def total_score(self) -> int: return self.count_booleans(self.ENDORSE_FIELDS) def distress_score(self) -> int: score = 0 for q in range(1, Caps.NQUESTIONS + 1): if getattr(self, "endorse" + str(q)) \ and getattr(self, "distress" + str(q)) is not None: score += self.sum_fields(["distress" + str(q)]) return score def intrusiveness_score(self) -> int: score = 0 for q in range(1, Caps.NQUESTIONS + 1): if getattr(self, "endorse" + str(q)) \ and getattr(self, "intrusiveness" + str(q)) is not None: score += self.sum_fields(["intrusiveness" + str(q)]) return score def frequency_score(self) -> int: score = 0 for q in range(1, Caps.NQUESTIONS + 1): if getattr(self, "endorse" + str(q)) \ and getattr(self, "frequency" + str(q)) is not None: score += self.sum_fields(["frequency" + str(q)]) return score def get_task_html(self, req: CamcopsRequest) -> str: total = self.total_score() distress = self.distress_score() intrusiveness = self.intrusiveness_score() frequency = self.frequency_score() q_a = "" for q in range(1, Caps.NQUESTIONS + 1): q_a += tr( self.wxstring(req, "q" + str(q)), answer(get_yes_no_none(req, getattr(self, "endorse" + str(q)))), answer( getattr(self, "distress" + str(q)) if getattr(self, "endorse" + str(q)) else ""), answer( getattr(self, "intrusiveness" + str(q)) if getattr(self, "endorse" + str(q)) else ""), answer( getattr(self, "frequency" + str(q)) if getattr(self, "endorse" + str(q)) else "")) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {distress} {intrusiveness} {frequency} </table> </div> <div class="{CssClass.EXPLANATION}"> Anchor points: DISTRESS {distress1}, {distress5}. INTRUSIVENESS {intrusiveness1}, {intrusiveness5}. FREQUENCY {frequency1}, {frequency5}. </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="10%">Endorsed?</th> <th width="10%">Distress (1–5)</th> <th width="10%">Intrusiveness (1–5)</th> <th width="10%">Frequency (1–5)</th> </tr> </table> <div class="{CssClass.FOOTNOTES}"> [1] Total score: sum of endorsements (yes = 1, no = 0). Dimension scores: sum of ratings (0 if not endorsed). (Bell et al. 2006, PubMed ID 16237200) </div> <div class="{CssClass.COPYRIGHT}"> CAPS: Copyright © 2005, Bell, Halligan & Ellis. Original article: Bell V, Halligan PW, Ellis HD (2006). The Cardiff Anomalous Perceptions Scale (CAPS): a new validated measure of anomalous perceptual experience. Schizophrenia Bulletin 32: 366–377. Published by Oxford University Press on behalf of the Maryland Psychiatric Research Center. All rights reserved. The online version of this article has been published under an open access model. Users are entitled to use, reproduce, disseminate, or display the open access version of this article for non-commercial purposes provided that: the original authorship is properly and fully attributed; the Journal and Oxford University Press are attributed as the original place of publication with the correct citation details given; if an article is subsequently reproduced or disseminated not in its entirety but only in part or as a derivative work this must be clearly indicated. For commercial re-use, please contact [email protected].<br> <b>This is a derivative work (partial reproduction, viz. the scale text).</b> </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr_qa( "{} <sup>[1]</sup> (0–32)".format( req.wappstring("total_score")), total), distress=tr_qa("{} (0–160)".format(self.wxstring(req, "distress")), distress), intrusiveness=tr_qa( "{} (0–160)".format(self.wxstring(req, "intrusiveness")), intrusiveness), frequency=tr_qa( "{} (0–160)".format(self.wxstring(req, "frequency")), frequency), distress1=self.wxstring(req, "distress_option1"), distress5=self.wxstring(req, "distress_option5"), intrusiveness1=self.wxstring(req, "intrusiveness_option1"), intrusiveness5=self.wxstring(req, "intrusiveness_option5"), frequency1=self.wxstring(req, "frequency_option1"), frequency5=self.wxstring(req, "frequency_option5"), ) return h
class Gad7(TaskHasPatientMixin, Task, metaclass=Gad7Metaclass): """ Server implementation of the GAD-7 task. """ __tablename__ = "gad7" shortname = "GAD-7" provides_trackers = True NQUESTIONS = 7 TASK_FIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = 21 @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Generalized Anxiety Disorder Assessment") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="GAD-7 total score", axis_label="Total score (out of 21)", axis_min=-0.5, axis_max=self.MAX_SCORE + 0.5, horizontal_lines=[14.5, 9.5, 4.5], horizontal_labels=[ TrackerLabel(17, req.sstring(SS.SEVERE)), TrackerLabel(12, req.sstring(SS.MODERATE)), TrackerLabel(7, req.sstring(SS.MILD)), TrackerLabel(2.25, req.sstring(SS.NONE)), ], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=( f"GAD-7 total score {self.total_score()}/{self.MAX_SCORE} " f"({self.severity(req)})")) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score (/{self.MAX_SCORE})", ), SummaryElement( name="severity", coltype=SummaryCategoryColType, value=self.severity(req), comment="Severity", ), ] def is_complete(self) -> bool: return (self.all_fields_not_none(self.TASK_FIELDS) and self.field_contents_valid()) def total_score(self) -> int: return self.sum_fields(self.TASK_FIELDS) def severity(self, req: CamcopsRequest) -> str: score = self.total_score() if score >= 15: severity = req.sstring(SS.SEVERE) elif score >= 10: severity = req.sstring(SS.MODERATE) elif score >= 5: severity = req.sstring(SS.MILD) else: severity = req.sstring(SS.NONE) return severity def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() severity = self.severity(req) answer_dict = {None: None} for option in range(0, 4): answer_dict[option] = (str(option) + " — " + self.wxstring(req, "a" + str(option))) q_a = "" for q in range(1, self.NQUESTIONS + 1): q_a += tr_qa( self.wxstring(req, "q" + str(q)), get_from_dict(answer_dict, getattr(self, "q" + str(q))), ) return """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {anxiety_severity} </table> </div> <div class="{CssClass.EXPLANATION}"> Ratings are over the last 2 weeks. </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] ≥15 severe, ≥10 moderate, ≥5 mild. Score ≥10 identifies: generalized anxiety disorder with sensitivity 89%, specificity 82% (Spitzer et al. 2006, PubMed ID 16717171); panic disorder with sensitivity 74%, specificity 81% (Kroenke et al. 2010, PMID 20633738); social anxiety with sensitivity 72%, specificity 80% (Kroenke et al. 2010); post-traumatic stress disorder with sensitivity 66%, specificity 81% (Kroenke et al. 2010). The majority of evidence contributing to these figures comes from primary care screening studies. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE), answer(score) + " / {}".format(self.MAX_SCORE), ), anxiety_severity=tr( self.wxstring(req, "anxiety_severity") + " <sup>[1]</sup>", severity, ), q_a=q_a, ) def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression(req.snomed( SnomedLookup.GAD7_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.GAD7_SCALE), {req.snomed(SnomedLookup.GAD7_SCORE): self.total_score()}, )) return codes
class Zbi12(TaskHasRespondentMixin, TaskHasPatientMixin, Task, metaclass=Zbi12Metaclass): """ Server implementation of the ZBI-12 task. """ __tablename__ = "zbi12" shortname = "ZBI-12" longname = "Zarit Burden Interview-12" MIN_PER_Q = 0 MAX_PER_Q = 4 NQUESTIONS = 12 TASK_FIELDS = strseq("q", 1, NQUESTIONS) MAX_TOTAL = MAX_PER_Q * NQUESTIONS def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment="Total score (/ {})".format(self.MAX_TOTAL) ), ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [CtvInfo(content="ZBI-12 total score {}/{}".format( self.total_score(), self.MAX_TOTAL))] def total_score(self) -> int: return self.sum_fields(self.TASK_FIELDS) def is_complete(self) -> bool: return ( self.field_contents_valid() and self.is_respondent_complete() and self.are_all_fields_complete(self.TASK_FIELDS) ) def get_task_html(self, req: CamcopsRequest) -> str: option_dict = {None: None} for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1): option_dict[a] = req.wappstring("zbi_a" + str(a)) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {complete_tr} <tr> <td>Total score (/ {maxtotal})</td> <td>{total}</td> </td> </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer ({minq}–{maxq})</th> </tr> """.format( CssClass=CssClass, complete_tr=self.get_is_complete_tr(req), total=answer(self.total_score()), maxtotal=self.MAX_TOTAL, minq=self.MIN_PER_Q, maxq=self.MAX_PER_Q, ) for q in range(1, self.NQUESTIONS + 1): a = getattr(self, "q" + str(q)) fa = ("{}: {}".format(a, get_from_dict(option_dict, a)) if a is not None else None) h += tr(self.wxstring(req, "q" + str(q)), answer(fa)) h += """ </table> """ + DATA_COLLECTION_UNLESS_UPGRADED_DIV return h
class Core10(TaskHasPatientMixin, Task): """ Server implementation of the CORE-10 task. """ __tablename__ = "core10" shortname = "CORE-10" provides_trackers = True COMMENT_NORMAL = " (0 not at all - 4 most or all of the time)" COMMENT_REVERSED = " (0 most or all of the time - 4 not at all)" q1 = CamcopsColumn( "q1", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q1 (tension/anxiety)" + COMMENT_NORMAL, ) q2 = CamcopsColumn( "q2", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q2 (support)" + COMMENT_REVERSED, ) q3 = CamcopsColumn( "q3", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q3 (coping)" + COMMENT_REVERSED, ) q4 = CamcopsColumn( "q4", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q4 (talking is too much)" + COMMENT_NORMAL, ) q5 = CamcopsColumn( "q5", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q5 (panic)" + COMMENT_NORMAL, ) q6 = CamcopsColumn( "q6", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q6 (suicidality)" + COMMENT_NORMAL, ) q7 = CamcopsColumn( "q7", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q7 (sleep problems)" + COMMENT_NORMAL, ) q8 = CamcopsColumn( "q8", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q8 (despair/hopelessness)" + COMMENT_NORMAL, ) q9 = CamcopsColumn( "q9", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q9 (unhappy)" + COMMENT_NORMAL, ) q10 = CamcopsColumn( "q10", Integer, permitted_value_checker=ZERO_TO_FOUR_CHECKER, comment="Q10 (unwanted images)" + COMMENT_NORMAL, ) N_QUESTIONS = 10 MAX_SCORE = 4 * N_QUESTIONS QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Clinical Outcomes in Routine Evaluation, 10-item measure") # noinspection PyMethodParameters @classproperty def minimum_client_version(cls) -> Version: return Version("2.2.8") def is_complete(self) -> bool: return self.all_fields_not_none(self.QUESTION_FIELDNAMES) def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.clinical_score(), plot_label="CORE-10 clinical score (rating distress)", axis_label=f"Clinical score (out of {self.MAX_SCORE})", axis_min=-0.5, axis_max=self.MAX_SCORE + 0.5, axis_ticks=[ TrackerAxisTick(40, "40"), TrackerAxisTick(35, "35"), TrackerAxisTick(30, "30"), TrackerAxisTick(25, "25"), TrackerAxisTick(20, "20"), TrackerAxisTick(15, "15"), TrackerAxisTick(10, "10"), TrackerAxisTick(5, "5"), TrackerAxisTick(0, "0"), ], horizontal_lines=[30, 20, 10], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo( content=( f"CORE-10 clinical score " f"{self.clinical_score()}/{self.MAX_SCORE}" ) ) ] # todo: CORE10: add suicidality to clinical text? def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="clinical_score", coltype=Integer(), value=self.clinical_score(), comment=f"Clinical score (/{self.MAX_SCORE})", ) ] def total_score(self) -> int: return self.sum_fields(self.QUESTION_FIELDNAMES) def n_questions_complete(self) -> int: return self.n_fields_not_none(self.QUESTION_FIELDNAMES) def clinical_score(self) -> float: n_q_completed = self.n_questions_complete() if n_q_completed == 0: # avoid division by zero return 0 return self.N_QUESTIONS * self.total_score() / n_q_completed def get_task_html(self, req: CamcopsRequest) -> str: normal_dict = { None: None, 0: "0 — " + self.wxstring(req, "a0"), 1: "1 — " + self.wxstring(req, "a1"), 2: "2 — " + self.wxstring(req, "a2"), 3: "3 — " + self.wxstring(req, "a3"), 4: "4 — " + self.wxstring(req, "a4"), } reversed_dict = { None: None, 0: "0 — " + self.wxstring(req, "a4"), 1: "1 — " + self.wxstring(req, "a3"), 2: "2 — " + self.wxstring(req, "a2"), 3: "3 — " + self.wxstring(req, "a1"), 4: "4 — " + self.wxstring(req, "a0"), } def get_tr_qa(qnum_: int, mapping: Dict[Optional[int], str]) -> str: nstr = str(qnum_) return tr_qa( self.wxstring(req, "q" + nstr), get_from_dict(mapping, getattr(self, "q" + nstr)), ) q_a = get_tr_qa(1, normal_dict) for qnum in (2, 3): q_a += get_tr_qa(qnum, reversed_dict) for qnum in range(4, self.N_QUESTIONS + 1): q_a += get_tr_qa(qnum, normal_dict) tr_clinical_score = tr( "Clinical score <sup>[1]</sup>", answer(self.clinical_score()) + " / {}".format(self.MAX_SCORE), ) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {tr_clinical_score} </table> </div> <div class="{CssClass.EXPLANATION}"> Ratings are over the last week. </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Clinical score is: number of questions × total score ÷ number of questions completed. If all questions are completed, it's just the total score. </div> """ def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.CORE10_PROCEDURE_ASSESSMENT) ) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.CORE10_SCALE), { req.snomed( SnomedLookup.CORE10_SCORE ): self.total_score() }, ) ) return codes
class CbiR(TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=CbiRMetaclass): """ Server implementation of the CBI-R task. """ __tablename__ = "cbir" shortname = "CBI-R" confirm_blanks = CamcopsColumn( "confirm_blanks", Integer, permitted_value_checker=BIT_CHECKER, comment="Respondent confirmed that blanks are deliberate (N/A) " "(0/NULL no, 1 yes)", ) comments = Column("comments", UnicodeText, comment="Additional comments") MIN_SCORE = 0 MAX_SCORE = 4 QNUMS_MEMORY = (1, 8) # tuple: first, last QNUMS_EVERYDAY = (9, 13) QNUMS_SELF = (14, 17) QNUMS_BEHAVIOUR = (18, 23) QNUMS_MOOD = (24, 27) QNUMS_BELIEFS = (28, 30) QNUMS_EATING = (31, 34) QNUMS_SLEEP = (35, 36) QNUMS_STEREOTYPY = (37, 40) QNUMS_MOTIVATION = (41, 45) NQUESTIONS = 45 TASK_FIELDS = strseq("frequency", 1, NQUESTIONS) + strseq( "distress", 1, NQUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Cambridge Behavioural Inventory, Revised") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="memory_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MEMORY), comment="Memory/orientation: frequency score (% of max)", ), SummaryElement( name="memory_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MEMORY), comment="Memory/orientation: distress score (% of max)", ), SummaryElement( name="everyday_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_EVERYDAY), comment="Everyday skills: frequency score (% of max)", ), SummaryElement( name="everyday_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_EVERYDAY), comment="Everyday skills: distress score (% of max)", ), SummaryElement( name="selfcare_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_SELF), comment="Self-care: frequency score (% of max)", ), SummaryElement( name="selfcare_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_SELF), comment="Self-care: distress score (% of max)", ), SummaryElement( name="behaviour_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_BEHAVIOUR), comment="Abnormal behaviour: frequency score (% of max)", ), SummaryElement( name="behaviour_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_BEHAVIOUR), comment="Abnormal behaviour: distress score (% of max)", ), SummaryElement( name="mood_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MOOD), comment="Mood: frequency score (% of max)", ), SummaryElement( name="mood_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MOOD), comment="Mood: distress score (% of max)", ), SummaryElement( name="beliefs_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_BELIEFS), comment="Beliefs: frequency score (% of max)", ), SummaryElement( name="beliefs_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_BELIEFS), comment="Beliefs: distress score (% of max)", ), SummaryElement( name="eating_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_EATING), comment="Eating habits: frequency score (% of max)", ), SummaryElement( name="eating_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_EATING), comment="Eating habits: distress score (% of max)", ), SummaryElement( name="sleep_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_SLEEP), comment="Sleep: frequency score (% of max)", ), SummaryElement( name="sleep_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_SLEEP), comment="Sleep: distress score (% of max)", ), SummaryElement( name="stereotypic_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_STEREOTYPY), comment="Stereotypic and motor behaviours: frequency " "score (% of max)", ), SummaryElement( name="stereotypic_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_STEREOTYPY), comment="Stereotypic and motor behaviours: distress " "score (% of max)", ), SummaryElement( name="motivation_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MOTIVATION), comment="Motivation: frequency score (% of max)", ), SummaryElement( name="motivation_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MOTIVATION), comment="Motivation: distress score (% of max)", ), ] def subscore(self, first: int, last: int, fieldprefix: str) -> Optional[float]: score = 0 n = 0 for q in range(first, last + 1): value = getattr(self, fieldprefix + str(q)) if value is not None: score += value / self.MAX_SCORE n += 1 return 100 * score / n if n > 0 else None def frequency_subscore(self, first: int, last: int) -> Optional[float]: return self.subscore(first, last, "frequency") def distress_subscore(self, first: int, last: int) -> Optional[float]: return self.subscore(first, last, "distress") def is_complete(self) -> bool: if (not self.field_contents_valid() or not self.is_respondent_complete()): return False if self.confirm_blanks: return True return self.all_fields_not_none(self.TASK_FIELDS) def get_task_html(self, req: CamcopsRequest) -> str: freq_dict = {None: None} distress_dict = {None: None} for a in range(self.MIN_SCORE, self.MAX_SCORE + 1): freq_dict[a] = self.wxstring(req, "f" + str(a)) distress_dict[a] = self.wxstring(req, "d" + str(a)) heading_memory = self.wxstring(req, "h_memory") heading_everyday = self.wxstring(req, "h_everyday") heading_selfcare = self.wxstring(req, "h_selfcare") heading_behaviour = self.wxstring(req, "h_abnormalbehaviour") heading_mood = self.wxstring(req, "h_mood") heading_beliefs = self.wxstring(req, "h_beliefs") heading_eating = self.wxstring(req, "h_eating") heading_sleep = self.wxstring(req, "h_sleep") heading_motor = self.wxstring(req, "h_stereotypy_motor") heading_motivation = self.wxstring(req, "h_motivation") def get_question_rows(first, last): html = "" for q in range(first, last + 1): f = getattr(self, "frequency" + str(q)) d = getattr(self, "distress" + str(q)) fa = (f"{f}: {get_from_dict(freq_dict, f)}" if f is not None else None) da = (f"{d}: {get_from_dict(distress_dict, d)}" if d is not None else None) html += tr(self.wxstring(req, "q" + str(q)), answer(fa), answer(da)) return html h = f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> <table class="{CssClass.SUMMARY}"> <tr> <th>Subscale</th> <th>Frequency (% of max)</th> <th>Distress (% of max)</th> </tr> <tr> <td>{heading_memory}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_MEMORY))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_MEMORY))}</td> </tr> <tr> <td>{heading_everyday}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_EVERYDAY))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_EVERYDAY))}</td> </tr> <tr> <td>{heading_selfcare}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_SELF))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_SELF))}</td> </tr> <tr> <td>{heading_behaviour}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR))}</td> </tr> <tr> <td>{heading_mood}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_MOOD))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_MOOD))}</td> </tr> <tr> <td>{heading_beliefs}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_BELIEFS))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_BELIEFS))}</td> </tr> <tr> <td>{heading_eating}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_EATING))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_EATING))}</td> </tr> <tr> <td>{heading_sleep}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_SLEEP))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_SLEEP))}</td> </tr> <tr> <td>{heading_motor}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_STEREOTYPY))}</td> </tr> <tr> <td>{heading_motivation}</td> <td>{answer(self.frequency_subscore(*self.QNUMS_MOTIVATION))}</td> <td>{answer(self.distress_subscore(*self.QNUMS_MOTIVATION))}</td> </tr> </table> </div> <table class="{CssClass.TASKDETAIL}"> {tr( "Respondent confirmed that blanks are deliberate (N/A)", answer(get_yes_no(req, self.confirm_blanks)) )} {tr("Comments", answer(self.comments, default=""))} </table> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="25%">Frequency (0–4)</th> <th width="25%">Distress (0–4)</th> </tr> {subheading_spanning_three_columns(heading_memory)} {get_question_rows(*self.QNUMS_MEMORY)} {subheading_spanning_three_columns(heading_everyday)} {get_question_rows(*self.QNUMS_EVERYDAY)} {subheading_spanning_three_columns(heading_selfcare)} {get_question_rows(*self.QNUMS_SELF)} {subheading_spanning_three_columns(heading_behaviour)} {get_question_rows(*self.QNUMS_BEHAVIOUR)} {subheading_spanning_three_columns(heading_mood)} {get_question_rows(*self.QNUMS_MOOD)} {subheading_spanning_three_columns(heading_beliefs)} {get_question_rows(*self.QNUMS_BELIEFS)} {subheading_spanning_three_columns(heading_eating)} {get_question_rows(*self.QNUMS_EATING)} {subheading_spanning_three_columns(heading_sleep)} {get_question_rows(*self.QNUMS_SLEEP)} {subheading_spanning_three_columns(heading_motor)} {get_question_rows(*self.QNUMS_STEREOTYPY)} {subheading_spanning_three_columns(heading_motivation)} {get_question_rows(*self.QNUMS_MOTIVATION)} </table> """ return h
class Smast(TaskHasPatientMixin, Task, metaclass=SmastMetaclass): """ Server implementation of the SMAST task. """ __tablename__ = "smast" shortname = "SMAST" longname = "Short Michigan Alcohol Screening Test" provides_trackers = True NQUESTIONS = 13 TASK_FIELDS = strseq("q", 1, NQUESTIONS) def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="SMAST total score", axis_label="Total score (out of {})".format(self.NQUESTIONS), axis_min=-0.5, axis_max=self.NQUESTIONS + 0.5, horizontal_lines=[ 2.5, 1.5, ], horizontal_labels=[ TrackerLabel(4, self.wxstring(req, "problem_probable")), TrackerLabel(2, self.wxstring(req, "problem_possible")), TrackerLabel(0.75, self.wxstring(req, "problem_unlikely")), ]) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content="SMAST total score {}/{} ({})".format( self.total_score(), self.NQUESTIONS, self.likelihood(req))) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="total", coltype=Integer(), value=self.total_score(), comment="Total score (/{})".format( self.NQUESTIONS)), SummaryElement(name="likelihood", coltype=SummaryCategoryColType, value=self.likelihood(req), comment="Likelihood of problem"), ] def is_complete(self) -> bool: return (self.are_all_fields_complete(self.TASK_FIELDS) and self.field_contents_valid()) def get_score(self, q: int) -> int: yes = "Y" value = getattr(self, "q" + str(q)) if value is None: return 0 if q == 1 or q == 4 or q == 5: return 0 if value == yes else 1 else: return 1 if value == yes else 0 def total_score(self) -> int: total = 0 for q in range(1, self.NQUESTIONS + 1): total += self.get_score(q) return total def likelihood(self, req: CamcopsRequest) -> str: score = self.total_score() if score >= 3: return self.wxstring(req, "problem_probable") elif score >= 2: return self.wxstring(req, "problem_possible") else: return self.wxstring(req, "problem_unlikely") def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() likelihood = self.likelihood(req) main_dict = { None: None, "Y": req.wappstring("yes"), "N": req.wappstring("no") } q_a = "" for q in range(1, self.NQUESTIONS + 1): q_a += tr( self.wxstring(req, "q" + str(q)), answer(get_from_dict(main_dict, getattr(self, "q" + str(q)))) + " — " + str(self.get_score(q))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {problem_likelihood} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Total score ≥3 probable, ≥2 possible, 0–1 unlikely. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr(req.wappstring("total_score"), answer(score) + " / {}".format(self.NQUESTIONS)), problem_likelihood=tr_qa( self.wxstring(req, "problem_likelihood") + " <sup>[1]</sup>", likelihood), q_a=q_a, ) return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: if not self.is_complete(): return [] return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]
class Honos65(HonosBase, metaclass=Honos65Metaclass): """ Server implementation of the HoNOS 65+ task. """ __tablename__ = "honos65" shortname = "HoNOS 65+" longname = "Health of the Nation Outcome Scales, older adults" q8problemtype = CamcopsColumn( "q8problemtype", CharColType, permitted_value_checker=PermittedValueChecker( permitted_values=PV_PROBLEMTYPE), comment="Q8: type of problem (A phobic; B anxiety; " "C obsessive-compulsive; D stress; " # NB slight difference: D "E dissociative; F somatoform; G eating; H sleep; " "I sexual; J other, specify)") q8otherproblem = Column("q8otherproblem", UnicodeText, comment="Q8: other problem: specify") NQUESTIONS = 12 QFIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = 48 # noinspection PyUnresolvedReferences def is_complete(self) -> bool: if not self.are_all_fields_complete(self.QFIELDS): return False if not self.field_contents_valid(): return False if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: return False if self.q8 != 0 and self.q8 != 9 and self.q8problemtype == "J" \ and self.q8otherproblem is None: return False return self.period_rated is not None def get_task_html(self, req: CamcopsRequest) -> str: q8_problem_type_dict = { None: None, "A": self.wxstring(req, "q8problemtype_option_a"), "B": self.wxstring(req, "q8problemtype_option_b"), "C": self.wxstring(req, "q8problemtype_option_c"), "D": self.wxstring(req, "q8problemtype_option_d"), "E": self.wxstring(req, "q8problemtype_option_e"), "F": self.wxstring(req, "q8problemtype_option_f"), "G": self.wxstring(req, "q8problemtype_option_g"), "H": self.wxstring(req, "q8problemtype_option_h"), "I": self.wxstring(req, "q8problemtype_option_i"), "J": self.wxstring(req, "q8problemtype_option_j"), } one_to_eight = "" for i in range(1, 8 + 1): one_to_eight += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i)))) nine_onwards = "" for i in range(9, Honos.NQUESTIONS + 1): nine_onwards += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i)))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer <sup>[1]</sup></th> </tr> {period_rated} {one_to_eight} {q8problemtype} {q8otherproblem} {nine_onwards} </table> <div class="{CssClass.FOOTNOTES}"> {FOOTNOTE_SCORING} </div> {copyright_div} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.wappstring("total_score"), answer(self.total_score()) + " / {}".format(self.MAX_SCORE)), period_rated=tr_qa(self.wxstring(req, "period_rated"), self.period_rated), one_to_eight=one_to_eight, q8problemtype=tr_qa( self.wxstring(req, "q8problemtype_s"), get_from_dict(q8_problem_type_dict, self.q8problemtype)), q8otherproblem=tr_qa(self.wxstring(req, "q8otherproblem_s"), self.q8otherproblem), nine_onwards=nine_onwards, FOOTNOTE_SCORING=FOOTNOTE_SCORING, copyright_div=self.COPYRIGHT_DIV, ) return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT)) ] # noqa if self.is_complete(): codes.append( SnomedExpression(req.snomed(SnomedLookup.HONOS65_SCALE), { req.snomed(SnomedLookup.HONOS65_SCORE): self.total_score(), })) return codes
class Ace3(TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=Ace3Metaclass): """ Server implementation of the ACE-III task. """ __tablename__ = "ace3" shortname = "ACE-III" provides_trackers = True prohibits_commercial = True age_at_leaving_full_time_education = Column( "age_at_leaving_full_time_education", Integer, comment="Age at leaving full time education", ) occupation = Column("occupation", UnicodeText, comment="Occupation") handedness = CamcopsColumn( "handedness", String(length=1), # was Text comment="Handedness (L or R)", permitted_value_checker=PermittedValueChecker( permitted_values=["L", "R"]), ) attn_num_registration_trials = Column( "attn_num_registration_trials", Integer, comment="Attention, repetition, number of trials (not scored)", ) fluency_letters_score = CamcopsColumn( "fluency_letters_score", Integer, comment="Fluency, words beginning with P, score 0-7", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7), ) fluency_animals_score = CamcopsColumn( "fluency_animals_score", Integer, comment="Fluency, animals, score 0-7", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7), ) lang_follow_command_practice = CamcopsColumn( "lang_follow_command_practice", Integer, comment="Language, command, practice trial (not scored)", permitted_value_checker=BIT_CHECKER, ) lang_read_words_aloud = CamcopsColumn( "lang_read_words_aloud", Integer, comment="Language, read five irregular words (0 or 1)", permitted_value_checker=BIT_CHECKER, ) vsp_copy_infinity = CamcopsColumn( "vsp_copy_infinity", Integer, comment="Visuospatial, copy infinity (0-1)", permitted_value_checker=BIT_CHECKER, ) vsp_copy_cube = CamcopsColumn( "vsp_copy_cube", Integer, comment="Visuospatial, copy cube (0-2)", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=2), ) vsp_draw_clock = CamcopsColumn( "vsp_draw_clock", Integer, comment="Visuospatial, draw clock (0-5)", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5), ) picture1_blobid = CamcopsColumn( "picture1_blobid", Integer, comment="Photo 1/2 PNG BLOB ID", is_blob_id_field=True, blob_relationship_attr_name="picture1", ) picture1_rotation = Column( # DEFUNCT as of v2.0.0 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE "picture1_rotation", Integer, comment="Photo 1/2 rotation (degrees clockwise)", ) picture2_blobid = CamcopsColumn( "picture2_blobid", Integer, comment="Photo 2/2 PNG BLOB ID", is_blob_id_field=True, blob_relationship_attr_name="picture2", ) picture2_rotation = Column( # DEFUNCT as of v2.0.0 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE "picture2_rotation", Integer, comment="Photo 2/2 rotation (degrees clockwise)", ) comments = Column("comments", UnicodeText, comment="Clinician's comments") picture1 = blob_relationship( "Ace3", "picture1_blobid") # type: Optional[Blob] # noqa picture2 = blob_relationship( "Ace3", "picture2_blobid") # type: Optional[Blob] # noqa ATTN_SCORE_FIELDS = (strseq("attn_time", 1, 5) + strseq("attn_place", 1, 5) + strseq("attn_repeat_word", 1, 3) + strseq("attn_serial7_subtraction", 1, 5)) MEM_NON_RECOG_SCORE_FIELDS = (strseq("mem_recall_word", 1, 3) + strseq("mem_repeat_address_trial3_", 1, 7) + strseq("mem_famous", 1, 4) + strseq("mem_recall_address", 1, 7)) LANG_SIMPLE_SCORE_FIELDS = (strseq("lang_write_sentences_point", 1, 2) + strseq("lang_repeat_sentence", 1, 2) + strseq("lang_name_picture", 1, 12) + strseq("lang_identify_concept", 1, 4)) LANG_FOLLOW_CMD_FIELDS = strseq("lang_follow_command", 1, 3) LANG_REPEAT_WORD_FIELDS = strseq("lang_repeat_word", 1, 4) VSP_SIMPLE_SCORE_FIELDS = strseq("vsp_count_dots", 1, 4) + strseq( "vsp_identify_letter", 1, 4) BASIC_COMPLETENESS_FIELDS = ( ATTN_SCORE_FIELDS + MEM_NON_RECOG_SCORE_FIELDS + ["fluency_letters_score", "fluency_animals_score"] + ["lang_follow_command_practice"] + LANG_SIMPLE_SCORE_FIELDS + LANG_REPEAT_WORD_FIELDS + [ "lang_read_words_aloud", "vsp_copy_infinity", "vsp_copy_cube", "vsp_draw_clock", ] + VSP_SIMPLE_SCORE_FIELDS + strseq("mem_recall_address", 1, 7)) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Addenbrooke’s Cognitive Examination III") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="ACE-III total score", axis_label="Total score (out of 100)", axis_min=-0.5, axis_max=100.5, horizontal_lines=[82.5, 88.5], ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v text = (f"ACE-III total: {t}/{TOTAL_MAX} " f"(attention {a}/{ATTN_MAX}, memory {m}/{MEMORY_MAX}, " f"fluency {f}/{FLUENCY_MAX}, language {lang}/{LANG_MAX}, " f"visuospatial {v}/{VSP_MAX})") return [CtvInfo(content=text)] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score (/{TOTAL_MAX})", ), SummaryElement( name="attn", coltype=Integer(), value=self.attn_score(), comment=f"Attention (/{ATTN_MAX})", ), SummaryElement( name="mem", coltype=Integer(), value=self.mem_score(), comment=f"Memory (/{MEMORY_MAX})", ), SummaryElement( name="fluency", coltype=Integer(), value=self.fluency_score(), comment=f"Fluency (/{FLUENCY_MAX})", ), SummaryElement( name="lang", coltype=Integer(), value=self.lang_score(), comment=f"Language (/{LANG_MAX})", ), SummaryElement( name="vsp", coltype=Integer(), value=self.vsp_score(), comment=f"Visuospatial (/{VSP_MAX})", ), ] def attn_score(self) -> int: return self.sum_fields(self.ATTN_SCORE_FIELDS) @staticmethod def get_recog_score(recalled: Optional[int], recognized: Optional[int]) -> int: if recalled == 1: return 1 return score_zero_for_absent(recognized) @staticmethod def get_recog_text(recalled: Optional[int], recognized: Optional[int]) -> str: if recalled: return "<i>1 (already recalled)</i>" return answer(recognized) # noinspection PyUnresolvedReferences def get_mem_recognition_score(self) -> int: score = 0 score += self.get_recog_score( (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), self.mem_recognize_address1, ) score += self.get_recog_score((self.mem_recall_address3 == 1), self.mem_recognize_address2) score += self.get_recog_score( (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), self.mem_recognize_address3, ) score += self.get_recog_score((self.mem_recall_address6 == 1), self.mem_recognize_address4) score += self.get_recog_score((self.mem_recall_address7 == 1), self.mem_recognize_address5) return score def mem_score(self) -> int: return (self.sum_fields(self.MEM_NON_RECOG_SCORE_FIELDS) + self.get_mem_recognition_score()) def fluency_score(self) -> int: return score_zero_for_absent( self.fluency_letters_score) + score_zero_for_absent( self.fluency_animals_score) def get_follow_command_score(self) -> int: if self.lang_follow_command_practice != 1: return 0 return self.sum_fields(self.LANG_FOLLOW_CMD_FIELDS) def get_repeat_word_score(self) -> int: n = self.sum_fields(self.LANG_REPEAT_WORD_FIELDS) return 2 if n >= 4 else (1 if n == 3 else 0) def lang_score(self) -> int: return (self.sum_fields(self.LANG_SIMPLE_SCORE_FIELDS) + self.get_follow_command_score() + self.get_repeat_word_score() + score_zero_for_absent(self.lang_read_words_aloud)) def vsp_score(self) -> int: return (self.sum_fields(self.VSP_SIMPLE_SCORE_FIELDS) + score_zero_for_absent(self.vsp_copy_infinity) + score_zero_for_absent(self.vsp_copy_cube) + score_zero_for_absent(self.vsp_draw_clock)) def total_score(self) -> int: return (self.attn_score() + self.mem_score() + self.fluency_score() + self.lang_score() + self.vsp_score()) # noinspection PyUnresolvedReferences def is_recognition_complete(self) -> bool: return ( ((self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1) or self.mem_recognize_address1 is not None) and (self.mem_recall_address3 == 1 or self.mem_recognize_address2 is not None) and ((self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1) or self.mem_recognize_address3 is not None) and (self.mem_recall_address6 == 1 or self.mem_recognize_address4 is not None) and (self.mem_recall_address7 == 1 or self.mem_recognize_address5 is not None)) def is_complete(self) -> bool: if self.any_fields_none(self.BASIC_COMPLETENESS_FIELDS): return False if not self.field_contents_valid(): return False if self.lang_follow_command_practice == 1 and self.any_fields_none( self.LANG_FOLLOW_CMD_FIELDS): return False return self.is_recognition_complete() # noinspection PyUnresolvedReferences def get_task_html(self, req: CamcopsRequest) -> str: def percent(score: int, maximum: int) -> str: return ws.number_to_dp(100 * score / maximum, PERCENT_DP) a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v if self.is_complete(): figsize = ( PlotDefaults.FULLWIDTH_PLOT_WIDTH / 3, PlotDefaults.FULLWIDTH_PLOT_WIDTH / 4, ) width = 0.9 fig = req.create_figure(figsize=figsize) ax = fig.add_subplot(1, 1, 1) scores = numpy.array([a, m, f, lang, v]) maxima = numpy.array( [ATTN_MAX, MEMORY_MAX, FLUENCY_MAX, LANG_MAX, VSP_MAX]) y = 100 * scores / maxima x_labels = ["Attn", "Mem", "Flu", "Lang", "VSp"] # noinspection PyTypeChecker n = len(y) xvar = numpy.arange(n) ax.bar(xvar, y, width, color="b") ax.set_ylabel("%", fontdict=req.fontdict) ax.set_xticks(xvar) x_offset = -0.5 ax.set_xlim(0 + x_offset, len(scores) + x_offset) ax.set_xticklabels(x_labels, fontdict=req.fontdict) fig.tight_layout() # or the ylabel drops off the figure # fig.autofmt_xdate() req.set_figure_font_sizes(ax) figurehtml = req.get_html_from_pyplot_figure(fig) else: figurehtml = "<i>Incomplete; not plotted</i>" return ( self.get_standard_clinician_comments_block(req, self.comments) + f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> <tr> {self.get_is_complete_td_pair(req)} <td class="{CssClass.FIGURE}" rowspan="7">{figurehtml}</td> </tr> """ + tr("Total ACE-III score <sup>[1]</sup>", answer(t) + " / 100") + tr( "Attention", answer(a) + f" / {ATTN_MAX} ({percent(a, ATTN_MAX)}%)", ) + tr( "Memory", answer(m) + f" / {MEMORY_MAX} ({percent(m, MEMORY_MAX)}%)", ) + tr( "Fluency", answer(f) + f" / {FLUENCY_MAX} ({percent(f, FLUENCY_MAX)}%)", ) + tr( "Language", answer(lang) + f" / {LANG_MAX} ({percent(lang, LANG_MAX)}%)", ) + tr( "Visuospatial", answer(v) + f" / {VSP_MAX} ({percent(v, VSP_MAX)}%)", ) + f""" </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer/score</td> </tr> """ + tr_qa( "Age on leaving full-time education", self.age_at_leaving_full_time_education, ) + tr_qa("Occupation", ws.webify(self.occupation)) + tr_qa("Handedness", ws.webify(self.handedness)) + subheading_spanning_two_columns("Attention") + tr( "Day? Date? Month? Year? Season?", ", ".join( answer(x) for x in ( self.attn_time1, self.attn_time2, self.attn_time3, self.attn_time4, self.attn_time5, )), ) + tr( "House number/floor? Street/hospital? Town? County? Country?", ", ".join( answer(x) for x in ( self.attn_place1, self.attn_place2, self.attn_place3, self.attn_place4, self.attn_place5, )), ) + tr( "Repeat: Lemon? Key? Ball?", ", ".join( answer(x) for x in ( self.attn_repeat_word1, self.attn_repeat_word2, self.attn_repeat_word3, )), ) + tr( "Repetition: number of trials <i>(not scored)</i>", answer(self.attn_num_registration_trials, formatter_answer=italic), ) + tr( "Serial subtractions: First correct? Second? Third? Fourth? " "Fifth?", ", ".join( answer(x) for x in ( self.attn_serial7_subtraction1, self.attn_serial7_subtraction2, self.attn_serial7_subtraction3, self.attn_serial7_subtraction4, self.attn_serial7_subtraction5, )), ) + subheading_spanning_two_columns("Memory (1)") + tr( "Recall: Lemon? Key? Ball?", ", ".join( answer(x) for x in ( self.mem_recall_word1, self.mem_recall_word2, self.mem_recall_word3, )), ) + subheading_spanning_two_columns("Fluency") + tr( "Score for words beginning with ‘P’ <i>(≥18 scores 7, 14–17 " "scores 6, 11–13 scores 5, 8–10 scores 4, 6–7 scores 3, " "4–5 scores 2, 2–3 scores 1, 0–1 scores 0)</i>", answer(self.fluency_letters_score) + " / 7", ) + tr( "Score for animals <i>(≥22 scores 7, 17–21 scores 6, " "14–16 scores 5, 11–13 scores 4, 9–10 scores 3, " "7–8 scores 2, 5–6 scores 1, <5 scores 0)</i>", answer(self.fluency_animals_score) + " / 7", ) + subheading_spanning_two_columns("Memory (2)") + tr( "Third trial of address registration: Harry? Barnes? 73? " "Orchard? Close? Kingsbridge? Devon?", ", ".join( answer(x) for x in ( self.mem_repeat_address_trial3_1, self.mem_repeat_address_trial3_2, self.mem_repeat_address_trial3_3, self.mem_repeat_address_trial3_4, self.mem_repeat_address_trial3_5, self.mem_repeat_address_trial3_6, self.mem_repeat_address_trial3_7, )), ) + tr( "Current PM? Woman who was PM? USA president? USA president " "assassinated in 1960s?", ", ".join( answer(x) for x in ( self.mem_famous1, self.mem_famous2, self.mem_famous3, self.mem_famous4, )), ) + subheading_spanning_two_columns("Language") + tr( "<i>Practice trial (“Pick up the pencil and then the " "paper”)</i>", answer(self.lang_follow_command_practice, formatter_answer=italic), ) + tr_qa( "“Place the paper on top of the pencil”", self.lang_follow_command1, ) + tr_qa( "“Pick up the pencil but not the paper”", self.lang_follow_command2, ) + tr_qa( "“Pass me the pencil after touching the paper”", self.lang_follow_command3, ) + tr( "Sentence-writing: point for ≥2 complete sentences about " "the one topic? Point for correct grammar and spelling?", ", ".join( answer(x) for x in ( self.lang_write_sentences_point1, self.lang_write_sentences_point2, )), ) + tr( "Repeat: caterpillar? eccentricity? unintelligible? " "statistician? <i>(score 2 if all correct, 1 if 3 correct, " "0 if ≤2 correct)</i>", "<i>{}, {}, {}, {}</i> (score <b>{}</b> / 2)".format( answer(self.lang_repeat_word1, formatter_answer=italic), answer(self.lang_repeat_word2, formatter_answer=italic), answer(self.lang_repeat_word3, formatter_answer=italic), answer(self.lang_repeat_word4, formatter_answer=italic), self.get_repeat_word_score(), ), ) + tr_qa( "Repeat: “All that glitters is not gold”?", self.lang_repeat_sentence1, ) + tr_qa( "Repeat: “A stitch in time saves nine”?", self.lang_repeat_sentence2, ) + tr( "Name pictures: spoon, book, kangaroo/wallaby", ", ".join( answer(x) for x in ( self.lang_name_picture1, self.lang_name_picture2, self.lang_name_picture3, )), ) + tr( "Name pictures: penguin, anchor, camel/dromedary", ", ".join( answer(x) for x in ( self.lang_name_picture4, self.lang_name_picture5, self.lang_name_picture6, )), ) + tr( "Name pictures: harp, rhinoceros/rhino, barrel/keg/tub", ", ".join( answer(x) for x in ( self.lang_name_picture7, self.lang_name_picture8, self.lang_name_picture9, )), ) + tr( "Name pictures: crown, alligator/crocodile, " "accordion/piano accordion/squeeze box", ", ".join( answer(x) for x in ( self.lang_name_picture10, self.lang_name_picture11, self.lang_name_picture12, )), ) + tr( "Identify pictures: monarchy? marsupial? Antarctic? nautical?", ", ".join( answer(x) for x in ( self.lang_identify_concept1, self.lang_identify_concept2, self.lang_identify_concept3, self.lang_identify_concept4, )), ) + tr_qa( "Read all successfully: sew, pint, soot, dough, height", self.lang_read_words_aloud, ) + subheading_spanning_two_columns("Visuospatial") + tr("Copy infinity", answer(self.vsp_copy_infinity) + " / 1") + tr("Copy cube", answer(self.vsp_copy_cube) + " / 2") + tr( "Draw clock with numbers and hands at 5:10", answer(self.vsp_draw_clock) + " / 5", ) + tr( "Count dots: 8, 10, 7, 9", ", ".join( answer(x) for x in ( self.vsp_count_dots1, self.vsp_count_dots2, self.vsp_count_dots3, self.vsp_count_dots4, )), ) + tr( "Identify letters: K, M, A, T", ", ".join( answer(x) for x in ( self.vsp_identify_letter1, self.vsp_identify_letter2, self.vsp_identify_letter3, self.vsp_identify_letter4, )), ) + subheading_spanning_two_columns("Memory (3)") + tr( "Recall address: Harry? Barnes? 73? Orchard? Close? " "Kingsbridge? Devon?", ", ".join( answer(x) for x in ( self.mem_recall_address1, self.mem_recall_address2, self.mem_recall_address3, self.mem_recall_address4, self.mem_recall_address5, self.mem_recall_address6, self.mem_recall_address7, )), ) + tr( "Recognize address: Jerry Barnes/Harry Barnes/Harry Bradford?", self.get_recog_text( (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), self.mem_recognize_address1, ), ) + tr( "Recognize address: 37/73/76?", self.get_recog_text( (self.mem_recall_address3 == 1), self.mem_recognize_address2, ), ) + tr( "Recognize address: Orchard Place/Oak Close/Orchard " "Close?", self.get_recog_text( (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), self.mem_recognize_address3, ), ) + tr( "Recognize address: Oakhampton/Kingsbridge/Dartington?", self.get_recog_text( (self.mem_recall_address6 == 1), self.mem_recognize_address4, ), ) + tr( "Recognize address: Devon/Dorset/Somerset?", self.get_recog_text( (self.mem_recall_address7 == 1), self.mem_recognize_address5, ), ) + subheading_spanning_two_columns("Photos of test sheet") + tr_span_col(get_blob_img_html(self.picture1), td_class=CssClass.PHOTO) + tr_span_col(get_blob_img_html(self.picture2), td_class=CssClass.PHOTO) + f""" </table> <div class="{CssClass.FOOTNOTES}"> [1] In the ACE-R (the predecessor of the ACE-III), scores ≤82 had sensitivity 0.84 and specificity 1.0 for dementia, and scores ≤88 had sensitivity 0.94 and specificity 0.89 for dementia, in a context of patients with AlzD, FTD, LBD, MCI, and controls (Mioshi et al., 2006, PMID 16977673). </div> <div class="{CssClass.COPYRIGHT}"> ACE-III: Copyright © 2012, John Hodges. “The ACE-III is available for free. The copyright is held by Professor John Hodges who is happy for the test to be used in clinical practice and research projects. There is no need to contact us if you wish to use the ACE-III in clinical practice.” (ACE-III FAQ, 7 July 2013, www.neura.edu.au). </div> """) def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT)) ] # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_ATTENTION_ORIENTATION) # noqa # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_MEMORY) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_FLUENCY) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_LANGUAGE) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_VISUOSPATIAL) if self.is_complete(): # could refine: is each subscale complete? a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v codes.append( SnomedExpression( req.snomed(SnomedLookup.ACE_R_SCALE), { req.snomed(SnomedLookup.ACE_R_SCORE): t, req.snomed(SnomedLookup.ACE_R_SUBSCORE_ATTENTION_ORIENTATION): a, # noqa req.snomed(SnomedLookup.ACE_R_SUBSCORE_MEMORY): m, req.snomed(SnomedLookup.ACE_R_SUBSCORE_FLUENCY): f, req.snomed(SnomedLookup.ACE_R_SUBSCORE_LANGUAGE): lang, req.snomed(SnomedLookup.ACE_R_SUBSCORE_VISUOSPATIAL): v, }, )) return codes
class Honosca(HonosBase, metaclass=HonoscaMetaclass): """ Server implementation of the HoNOSCA task. """ __tablename__ = "honosca" shortname = "HoNOSCA" longname = "Health of the Nation Outcome Scales, Children and Adolescents" NQUESTIONS = 15 QFIELDS = strseq("q", 1, NQUESTIONS) LAST_SECTION_A_Q = 13 FIRST_SECTION_B_Q = 14 SECTION_A_QFIELDS = strseq("q", 1, LAST_SECTION_A_Q) SECTION_B_QFIELDS = strseq("q", FIRST_SECTION_B_Q, NQUESTIONS) MAX_SCORE = 60 MAX_SECTION_A = 4 * len(SECTION_A_QFIELDS) MAX_SECTION_B = 4 * len(SECTION_B_QFIELDS) TASK_FIELDS = QFIELDS + ["period_rated"] def is_complete(self) -> bool: return (self.are_all_fields_complete(self.TASK_FIELDS) and self.field_contents_valid()) def section_a_score(self) -> int: return self._total_score_for_fields(self.SECTION_A_QFIELDS) def section_b_score(self) -> int: return self._total_score_for_fields(self.SECTION_B_QFIELDS) def get_task_html(self, req: CamcopsRequest) -> str: section_a = "" for i in range(1, 13 + 1): section_a += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i)))) section_b = "" for i in range(14, self.NQUESTIONS + 1): section_b += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i)))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {section_a_total} {section_b_total} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer <sup>[1]</sup></th> </tr> {period_rated} {section_a_subhead} {section_a} {section_b_subhead} {section_b} </table> <div class="{CssClass.FOOTNOTES}"> {FOOTNOTE_SCORING} </div> {copyright_div} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.wappstring("total_score"), answer(self.total_score()) + " / {}".format(self.MAX_SCORE)), section_a_total=tr( self.wxstring(req, "section_a_total"), answer(self.section_a_score()) + " / {}".format(self.MAX_SECTION_A)), section_b_total=tr( self.wxstring(req, "section_b_total"), answer(self.section_b_score()) + " / {}".format(self.MAX_SECTION_B)), period_rated=tr_qa(self.wxstring(req, "period_rated"), self.period_rated), section_a_subhead=subheading_spanning_two_columns( self.wxstring(req, "section_a_title")), section_a=section_a, section_b_subhead=subheading_spanning_two_columns( self.wxstring(req, "section_b_title")), section_b=section_b, FOOTNOTE_SCORING=FOOTNOTE_SCORING, copyright_div=self.COPYRIGHT_DIV, ) return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.HONOSCA_PROCEDURE_ASSESSMENT)) ] # noqa if self.is_complete(): a = self.section_a_score() b = self.section_b_score() total = a + b codes.append( SnomedExpression( req.snomed(SnomedLookup.HONOSCA_SCALE), { req.snomed(SnomedLookup.HONOSCA_SCORE): total, req.snomed(SnomedLookup.HONOSCA_SECTION_A_SCORE): a, req.snomed(SnomedLookup.HONOSCA_SECTION_B_SCORE): b, req.snomed(SnomedLookup.HONOSCA_SECTION_A_PLUS_B_SCORE): total, # noqa })) return codes