def get_task_html(self, req: CamcopsRequest) -> str: strongly_disagree = self.wxstring(req, "strongly_disagree") disagree = self.wxstring(req, "disagree") agree = self.wxstring(req, "agree") # We store the actual answers given but these are scored 1 or 0 forward_answer_dict = { None: None, self.STRONGLY_DISAGREE: "1 — " + strongly_disagree, self.DISAGREE: "1 — " + disagree, self.AGREE: "0 — " + agree, self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " + self.wxstring(req, "strongly_agree"), } # Subtle difference in wording when options presented in reverse reverse_answer_dict = { None: None, self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " + self.wxstring(req, "definitely_agree"), self.AGREE: "0 — " + agree, self.DISAGREE: "1 — " + disagree, self.STRONGLY_DISAGREE: "1 — " + strongly_disagree, } 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)) answer_dict = forward_answer_dict if q_num in self.REVERSE_QUESTIONS: answer_dict = reverse_answer_dict answer_cell = get_from_dict(answer_dict, getattr(self, q_field)) rows += tr_qa(question_cell, answer_cell) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer</th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Score 1 point for either ‘disagree’ option, 0 points for either ‘agree’ option. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>", "{} / {}".format(answer(self.total_score()), self.MAX_SCORE), ), rows=rows, ) return html
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
def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() category = self.category(req) h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {category} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Score</th> </tr> """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE), answer(score) + f" / {self.MAX_SCORE}", ), category=tr_qa( req.sstring(SS.CATEGORY) + " <sup>[1]</sup>", category), ) h += tr_qa(self.wxstring(req, "alert_s"), get_yes_no_none(req, self.alert)) h += tr_qa( self.wxstring(req, "highschool_s"), get_yes_no_none(req, self.highschooleducation), ) h += tr_qa(self.wxstring(req, "q1_s"), self.q1) h += tr_qa(self.wxstring(req, "q2_s"), self.q2) h += tr_qa(self.wxstring(req, "q3_s"), self.q3) h += tr( "Q5 <sup>[2]</sup> (money spent, money left " "[<i>scores 2</i>]", ", ".join(answer(x) for x in (self.q5a, self.q5b)), ) h += tr_qa( "Q6 (animal fluency) [<i>≥15 scores 3, 10–14 scores 2, " "5–9 scores 1, 0–4 scores 0</i>]", self.q6, ) h += tr( "Q7 (recall: apple, pen, tie, house, car)", ", ".join( answer(x) for x in (self.q7a, self.q7b, self.q7c, self.q7d, self.q7e)), ) h += tr( "Q8 (backwards: 648, 8537)", ", ".join(answer(x) for x in (self.q8b, self.q8c)), ) h += tr( "Q9 (clock: hour markers, time [<i>score 2 each</i>]", ", ".join(answer(x) for x in (self.q9a, self.q9b)), ) h += tr( "Q10 (X in triangle; which is biggest?)", ", ".join(answer(x) for x in (self.q10a, self.q10b)), ) h += tr( "Q11 (story: Female’s name? Job? When back to work? " "State she lived in? [<i>score 2 each</i>])", ", ".join( answer(x) for x in (self.q11a, self.q11b, self.q11c, self.q11d)), ) h += f""" </table> <table class="{CssClass.TASKDETAIL}"> """ h += subheading_spanning_two_columns("Images of tests: clock, shapes") # noinspection PyTypeChecker h += tr( td( get_blob_img_html(self.clockpicture), td_width="50%", td_class=CssClass.PHOTO, ), td( get_blob_img_html(self.shapespicture), td_width="50%", td_class=CssClass.PHOTO, ), literal=True, ) h += f""" </table> <div class="{CssClass.FOOTNOTES}"> [1] With high school education: ≥27 normal, ≥21 MCI, ≤20 dementia. Without high school education: ≥25 normal, ≥20 MCI, ≤19 dementia. (Tariq et al. 2006, PubMed ID 17068312.) [2] Q4 (learning the five words) isn’t scored. </div> """ return h
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_task_html(self, req: CamcopsRequest) -> str: dict_q1 = {None: None} dict_q3 = {None: None} dict_q4 = {None: None} dict_q5 = {None: None} dict_q11 = {None: None} dict_q12 = {None: None} for option in range(1, 5): dict_q1[option] = self.wxstring(req, "q1_option" + str(option)) for option in range(1, 6): dict_q3[option] = self.wxstring(req, "q3_option" + str(option)) dict_q11[option] = self.wxstring(req, "q11_option" + str(option)) for option in range(0, 6): prefix = str(option) + " – " if option > 0 else "" dict_q4[option] = prefix + self.wxstring(req, "q4_option" + str(option)) dict_q5[option] = prefix + self.wxstring(req, "q5_option" + str(option)) for option in range(1, 17): dict_q12[option] = self.wxstring(req, "ethnicity_option" + str(option)) h = f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer</th> </tr> """ ell = "… " # horizontal ellipsis sep_row = subheading_spanning_two_columns("<br>") blank_cell = td("", td_class=CssClass.SUBHEADING) h += tr_qa(self.wxstring(req, "q_doctor"), ws.webify(self.doctor)) h += sep_row h += tr_qa(self.wxstring(req, "q1"), get_from_dict(dict_q1, self.q1)) h += tr(td(self.wxstring(req, "q2")), blank_cell, literal=True) h += tr_qa( ell + self.wxstring(req, "q2_a"), get_yes_no_none(req, self.q2a), default="", ) h += tr_qa( ell + self.wxstring(req, "q2_b"), get_yes_no_none(req, self.q2b), default="", ) h += tr_qa( ell + self.wxstring(req, "q2_c"), get_yes_no_none(req, self.q2c), default="", ) h += tr_qa( ell + self.wxstring(req, "q2_d"), get_yes_no_none(req, self.q2d), default="", ) h += tr_qa( ell + self.wxstring(req, "q2_e"), get_yes_no_none(req, self.q2e), default="", ) h += tr_qa( ell + self.wxstring(req, "q2_f"), get_yes_no_none(req, self.q2f), default="", ) h += tr_qa( ell + ell + self.wxstring(req, "q2f_s"), ws.webify(self.q2f_details), ) h += tr_qa(self.wxstring(req, "q3"), get_from_dict(dict_q3, self.q3)) h += tr(td(self.wxstring(req, "q4")), blank_cell, literal=True) h += tr_qa(ell + self.wxstring(req, "q4_a"), get_from_dict(dict_q4, self.q4a)) h += tr_qa(ell + self.wxstring(req, "q4_b"), get_from_dict(dict_q4, self.q4b)) h += tr_qa(ell + self.wxstring(req, "q4_c"), get_from_dict(dict_q4, self.q4c)) h += tr_qa(ell + self.wxstring(req, "q4_d"), get_from_dict(dict_q4, self.q4d)) h += tr_qa(ell + self.wxstring(req, "q4_e"), get_from_dict(dict_q4, self.q4e)) h += tr_qa(ell + self.wxstring(req, "q4_f"), get_from_dict(dict_q4, self.q4f)) h += tr_qa(ell + self.wxstring(req, "q4_g"), get_from_dict(dict_q4, self.q4g)) h += tr(td(self.wxstring(req, "q5")), blank_cell, literal=True) h += tr_qa(ell + self.wxstring(req, "q5_a"), get_from_dict(dict_q5, self.q5a)) h += tr_qa(ell + self.wxstring(req, "q5_b"), get_from_dict(dict_q5, self.q5b)) h += tr_qa(self.wxstring(req, "q6"), get_yes_no_none(req, self.q6)) h += tr_qa(self.wxstring(req, "q7"), get_yes_no_none(req, self.q7)) h += tr_qa(self.wxstring(req, "q8"), get_yes_no_none(req, self.q8)) h += tr_qa(self.wxstring(req, "q9_s"), ws.webify(self.q9)) h += sep_row h += tr_qa(req.sstring(SS.SEX), ws.webify(self.q10)) h += tr_qa(self.wxstring(req, "q11"), get_from_dict(dict_q11, self.q11)) h += tr_qa(self.wxstring(req, "q12"), get_from_dict(dict_q12, self.q12)) h += tr_qa( ell + self.wxstring(req, "ethnicity_other_s"), ws.webify(self.q12_details), ) h += """ </table> """ return h
def get_trial_html(self, req: CamcopsRequest) -> str: trialarray = self.trials # type: List[CardinalExpDetThresholdTrial] # for type hinter! # noqa html = CardinalExpDetThresholdTrial.get_html_table_header() for t in trialarray: html += t.get_html_table_row() html += """</table>""" # Don't add figures if we're incomplete if not self.is_complete(): return html # Add figures figsize = (FULLWIDTH_PLOT_WIDTH / 2, FULLWIDTH_PLOT_WIDTH / 2) jitter_step = 0.02 dp_to_consider_same_for_jitter = 3 y_extra_space = 0.1 x_extra_space = 0.02 trialfig = req.create_figure(figsize=figsize) trialax = trialfig.add_subplot(WHOLE_PANEL) notcalc_detected_x = [] notcalc_detected_y = [] notcalc_missed_x = [] notcalc_missed_y = [] calc_detected_x = [] calc_detected_y = [] calc_missed_x = [] calc_missed_y = [] catch_detected_x = [] catch_detected_y = [] catch_missed_x = [] catch_missed_y = [] all_x = [] all_y = [] for t in trialarray: x = t.trial y = t.intensity all_x.append(x) all_y.append(y) if t.trial_num_in_calculation_sequence is not None: if t.yes: calc_detected_x.append(x) calc_detected_y.append(y) else: calc_missed_x.append(x) calc_missed_y.append(y) elif t.target_presented: if t.yes: notcalc_detected_x.append(x) notcalc_detected_y.append(y) else: notcalc_missed_x.append(x) notcalc_missed_y.append(y) else: # catch trial if t.yes: catch_detected_x.append(x) catch_detected_y.append(y) else: catch_missed_x.append(x) catch_missed_y.append(y) trialax.plot(all_x, all_y, marker="", color="0.9", linestyle="-", label=None) trialax.plot(notcalc_missed_x, notcalc_missed_y, marker="o", color="k", linestyle="None", label="miss") trialax.plot(notcalc_detected_x, notcalc_detected_y, marker="+", color="k", linestyle="None", label="hit") trialax.plot(calc_missed_x, calc_missed_y, marker="o", color="r", linestyle="None", label="miss, scored") trialax.plot(calc_detected_x, calc_detected_y, marker="+", color="b", linestyle="None", label="hit, scored") trialax.plot(catch_missed_x, catch_missed_y, marker="o", color="g", linestyle="None", label="CR") trialax.plot(catch_detected_x, catch_detected_y, marker="*", color="g", linestyle="None", label="FA") leg = trialax.legend( numpoints=1, fancybox=True, # for set_alpha (below) loc="best", # bbox_to_anchor=(0.75, 1.05) labelspacing=0, handletextpad=0, prop=req.fontprops) leg.get_frame().set_alpha(0.5) trialax.set_xlabel("Trial number (0-based)", fontdict=req.fontdict) trialax.set_ylabel("Intensity", fontdict=req.fontdict) trialax.set_ylim(0 - y_extra_space, 1 + y_extra_space) trialax.set_xlim(-0.5, len(trialarray) - 0.5) req.set_figure_font_sizes(trialax) fitfig = None if self.k is not None and self.theta is not None: fitfig = req.create_figure(figsize=figsize) fitax = fitfig.add_subplot(WHOLE_PANEL) detected_x = [] detected_x_approx = [] detected_y = [] missed_x = [] missed_x_approx = [] missed_y = [] all_x = [] for t in trialarray: if t.trial_num_in_calculation_sequence is not None: all_x.append(t.intensity) approx_x = "{0:.{precision}f}".format( t.intensity, precision=dp_to_consider_same_for_jitter) if t.yes: detected_y.append(1 - detected_x_approx.count(approx_x) * jitter_step) detected_x.append(t.intensity) detected_x_approx.append(approx_x) else: missed_y.append(0 + missed_x_approx.count(approx_x) * jitter_step) missed_x.append(t.intensity) missed_x_approx.append(approx_x) fit_x = np.arange(0.0 - x_extra_space, 1.0 + x_extra_space, 0.001) fit_y = logistic(fit_x, self.k, self.theta) fitax.plot(fit_x, fit_y, color="g", linestyle="-") fitax.plot(missed_x, missed_y, marker="o", color="r", linestyle="None") fitax.plot(detected_x, detected_y, marker="+", color="b", linestyle="None") fitax.set_ylim(0 - y_extra_space, 1 + y_extra_space) fitax.set_xlim( np.amin(all_x) - x_extra_space, np.amax(all_x) + x_extra_space) marker_points = [] for y in (LOWER_MARKER, 0.5, UPPER_MARKER): x = inv_logistic(y, self.k, self.theta) marker_points.append((x, y)) for p in marker_points: fitax.plot([p[0], p[0]], [-1, p[1]], color="0.5", linestyle=":") fitax.plot([-1, p[0]], [p[1], p[1]], color="0.5", linestyle=":") fitax.set_xlabel("Intensity", fontdict=req.fontdict) fitax.set_ylabel("Detected? (0=no, 1=yes; jittered)", fontdict=req.fontdict) req.set_figure_font_sizes(fitax) html += """ <table class="{CssClass.NOBORDER}"> <tr> <td class="{CssClass.NOBORDERPHOTO}">{trialfig}</td> <td class="{CssClass.NOBORDERPHOTO}">{fitfig}</td> </tr> </table> """.format(CssClass=CssClass, trialfig=req.get_html_from_pyplot_figure(trialfig), fitfig=req.get_html_from_pyplot_figure(fitfig)) return html
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
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))) ) h = """ <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.wappstring("total_score"), answer(score) + " / {}".format(self.MAX_SCORE) ), anxiety_severity=tr( self.wxstring(req, "anxiety_severity") + " <sup>[1]</sup>", severity ), q_a=q_a, ) return h
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 = (FULLWIDTH_PLOT_WIDTH / 3, 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) + """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> <tr> {is_complete} <td class="{CssClass.FIGURE}" rowspan="7">{figurehtml}</td> </tr> """.format( # noqa CssClass=CssClass, is_complete=self.get_is_complete_td_pair(req), figurehtml=figurehtml) + tr("Total ACE-III score <sup>[1]</sup>", answer(t) + " / 100") + tr( "Attention", answer(a) + " / {} ({}%)".format(ATTN_MAX, percent(a, ATTN_MAX))) + tr( "Memory", answer(m) + " / {} ({}%)".format(MEMORY_MAX, percent(m, MEMORY_MAX))) + tr( "Fluency", answer(f) + " / {} ({}%)".format(FLUENCY_MAX, percent(f, FLUENCY_MAX))) + tr( "Language", answer(lang) + " / {} ({}%)".format(LANG_MAX, percent(lang, LANG_MAX))) + tr("Visuospatial", answer(v) + " / {} ({}%)".format(VSP_MAX, percent(v, VSP_MAX))) + """ </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer/score</td> </tr> """.format(CssClass=CssClass) + 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) + """ </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> """.format(CssClass=CssClass))
def get_task_html(self, req: CamcopsRequest) -> str: p = self.score_p() n = self.score_n() g = self.score_g() composite = self.composite() total = p + n + g answers = { None: None, 1: self.wxstring(req, "option1"), 2: self.wxstring(req, "option2"), 3: self.wxstring(req, "option3"), 4: self.wxstring(req, "option4"), 5: self.wxstring(req, "option5"), 6: self.wxstring(req, "option6"), 7: self.wxstring(req, "option7"), } q_a = "" for q in self.TASK_FIELDS: q_a += tr_qa(self.wxstring(req, "" + q + "_s"), get_from_dict(answers, getattr(self, q))) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {p} {n} {g} {composite} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="40%">Question</th> <th width="60%">Answer</th> </tr> {q_a} </table> {DATA_COLLECTION_ONLY_DIV} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr_qa( "{} ({}–{})".format(req.wappstring("total_score"), self.MIN_TOTAL, self.MAX_TOTAL), total), p=tr_qa( "{} ({}–{})".format(self.wxstring(req, "p"), self.MIN_P, self.MAX_P), p), n=tr_qa( "{} ({}–{})".format(self.wxstring(req, "n"), self.MIN_N, self.MAX_N), n), g=tr_qa( "{} ({}–{})".format(self.wxstring(req, "g"), self.MIN_G, self.MAX_G), g), composite=tr_qa( "{} ({}–{})".format(self.wxstring(req, "composite"), self.MIN_P_MINUS_N, self.MAX_P_MINUS_N), composite), q_a=q_a, DATA_COLLECTION_ONLY_DIV=DATA_COLLECTION_ONLY_DIV, ) return h
def get_task_html(self, req: CamcopsRequest) -> str: h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} </table> </div> <div class="{CssClass.EXPLANATION}"> 1. Simple discrimination (SD), and 2. reversal (SDr); 3. compound discrimination (CD), and 4. reversal (CDr); 5. intradimensional shift (ID), and 6. reversal (IDr); 7. extradimensional shift (ED), and 8. reversal (EDr). </div> <table class="{CssClass.TASKCONFIG}"> <tr> <th width="50%">Configuration variable</th> <th width="50%">Value</th> </tr> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), ) h += tr_qa(self.wxstring(req, "last_stage"), self.last_stage) h += tr_qa(self.wxstring(req, "max_trials_per_stage"), self.max_trials_per_stage) h += tr_qa(self.wxstring(req, "progress_criterion_x"), self.progress_criterion_x) h += tr_qa(self.wxstring(req, "progress_criterion_y"), self.progress_criterion_y) h += tr_qa(self.wxstring(req, "min_number"), self.min_number) h += tr_qa(self.wxstring(req, "max_number"), self.max_number) h += tr_qa(self.wxstring(req, "pause_after_beep_ms"), self.pause_after_beep_ms) h += tr_qa(self.wxstring(req, "iti_ms"), self.iti_ms) h += tr_qa( self.wxstring(req, "counterbalance_dimensions") + "<sup>[1]</sup>", self.counterbalance_dimensions) h += tr_qa(req.wappstring("volume_0_to_1"), self.volume) h += tr_qa(self.wxstring(req, "offer_abort"), self.offer_abort) h += tr_qa(self.wxstring(req, "debug_display_stimuli_only"), self.debug_display_stimuli_only) h += tr_qa( "Shapes (as a JSON-encoded array of SVG " "definitions; X and Y range both –60 to +60)", ws.webify(self.shape_definitions_svg)) h += """ </table> <table class="{CssClass.TASKDETAIL}"> <tr><th width="50%">Measure</th><th width="50%">Value</th></tr> """.format(CssClass=CssClass) h += tr_qa("Aborted?", get_yes_no_none(req, self.aborted)) h += tr_qa("Finished?", get_yes_no_none(req, self.finished)) h += tr_qa("Last trial completed", self.last_trial_completed) h += (""" </table> <div>Stage specifications and results:</div> """ + self.get_stage_html() + "<div>Trial-by-trial results:</div>" + self.get_trial_html() + """ <div class="{CssClass.FOOTNOTES}"> [1] Counterbalancing of dimensions is as follows, with notation X/Y indicating that X is the first relevant dimension (for stages SD–IDr) and Y is the second relevant dimension (for stages ED–EDr). 0: shape/colour. 1: colour/number. 2: number/shape. 3: shape/number. 4: colour/shape. 5: number/colour. </div> """.format(CssClass=CssClass)) return h
def get_task_html(self, req: CamcopsRequest) -> str: score = self.total_score() # Suicidal thoughts: suicidality_score = getattr(self, SUICIDALITY_FN) if suicidality_score is None: suicidality_text = bold("? (not completed)") suicidality_css_class = CssClass.INCOMPLETE elif suicidality_score == 0: suicidality_text = str(suicidality_score) suicidality_css_class = "" else: suicidality_text = bold(str(suicidality_score)) suicidality_css_class = CssClass.WARNING # Custom somatic score for Khandaker Insight study: somatic_css_class = "" if self.is_bdi_ii(): somatic_values = self.get_values( CUSTOM_SOMATIC_KHANDAKER_BDI_II_FIELDS) somatic_missing = False somatic_score = 0 for v in somatic_values: if v is None: somatic_missing = True somatic_css_class = CssClass.INCOMPLETE break else: somatic_score += int(v) somatic_text = ("incomplete" if somatic_missing else str(somatic_score)) else: somatic_text = "N/A" # not the BDI-II # Question rows: q_a = "" qdict = TOPICS_BY_SCALE.get(self.bdi_scale) topic = "?" for q in range(1, NQUESTIONS + 1): if qdict: topic = qdict.get(q, "??") q_a += tr_qa( "{question} {qnum} ({topic})".format( question=req.wappstring("question"), qnum=q, topic=topic, ), getattr(self, "q" + str(q)) ) # HTML: h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {tr_total_score} <tr> <td> Suicidal thoughts/wishes score (Q{suicidality_qnum}) <sup>[1]</sup> </td> {td_suicidality} </tr> {tr_somatic_score} </table> </div> <div class="{CssClass.EXPLANATION}"> All questions are scored from 0–3 (0 free of symptoms, 3 most symptomatic). </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="70%">Question</th> <th width="30%">Answer</th> </tr> {tr_which_scale} {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Suicidal thoughts are asked about in Q{suicidality_qnum} for all of: BDI-I (1961), BDI-IA (1978), and BDI-II (1996). [2] Insight study: <a href="https://doi.org/10.1186/ISRCTN16942542">doi:10.1186/ISRCTN16942542</a> [3] See the <a href="https://camcops.readthedocs.io/en/latest/tasks/bdi.html">CamCOPS BDI help</a> for full references and bibliography for the citations that follow. <b>The BDI rates “right now” [Beck1988]. The BDI-IA rates the past week [Beck1988]. The BDI-II rates the past two weeks [Beck1996b].</b> 1961 BDI(-I) question topics from [Beck1988]. 1978 BDI-IA question topics from [Beck1996b]. 1996 BDI-II question topics from [Steer1999], [Gary2018]. </ul> </div> {data_collection_only_div} """.format( # noqa CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), tr_total_score=tr( req.wappstring("total_score"), answer(score) + " / {}".format(MAX_SCORE) ), suicidality_qnum=SUICIDALITY_QNUM, td_suicidality=td(suicidality_text, td_class=suicidality_css_class), # noqa tr_somatic_score=tr( td( "Custom somatic score for Insight study <sup>[2]</sup> " "(sum of scores for questions {}, for BDI-II only)".format( ", ".join("Q" + str(qnum) for qnum in CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS)) ), td(somatic_text, td_class=somatic_css_class), literal=True ), tr_which_scale=tr_qa( req.wappstring("bdi_which_scale") + " <sup>[3]</sup>", ws.webify(self.bdi_scale) ), q_a=q_a, data_collection_only_div=DATA_COLLECTION_ONLY_DIV ) return h
def get_task_html(self, req: CamcopsRequest) -> str: h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {date_pertains_to} {category} {n_core} {n_total} {n_somatic} {psychotic_symptoms_or_stupor} </table> </div> <div class="{CssClass.EXPLANATION}"> {icd10_symptomatic_disclaimer} </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), date_pertains_to=tr_qa( req.wappstring("date_pertains_to"), format_datetime(self.date_pertains_to, DateFormat.LONG_DATE, default=None)), category=tr_qa( req.wappstring("category") + " <sup>[1,2]</sup>", self.get_full_description(req)), n_core=tr(self.wxstring(req, "n_core"), answer(self.n_core()) + " / 3"), n_total=tr(self.wxstring(req, "n_total"), answer(self.n_total()) + " / 10"), n_somatic=tr(self.wxstring(req, "n_somatic"), answer(self.n_somatic()) + " / 8"), psychotic_symptoms_or_stupor=tr( self.wxstring(req, "psychotic_symptoms_or_stupor") + " <sup>[2]</sup>", answer( get_present_absent_none(req, self.is_psychotic_or_stupor()))), icd10_symptomatic_disclaimer=req.wappstring( "icd10_symptomatic_disclaimer"), ) h += self.text_row(req, "duration_text") h += self.row_true_false(req, "duration_at_least_2_weeks") h += self.text_row(req, "core") for x in self.CORE_NAMES: h += self.row_present_absent(req, x) h += self.text_row(req, "additional") for x in self.ADDITIONAL_NAMES: h += self.row_present_absent(req, x) h += self.text_row(req, "clinical_text") h += self.row_true_false(req, "severe_clinically") h += self.text_row(req, "somatic") for x in self.SOMATIC_NAMES: h += self.row_present_absent(req, x) h += self.text_row(req, "psychotic") for x in self.PSYCHOSIS_NAMES: h += self.row_present_absent(req, x) h += """ </table> <div class="{CssClass.FOOTNOTES}"> [1] Mild depression requires ≥2 core symptoms and ≥4 total diagnostic symptoms. Moderate depression requires ≥2 core and ≥6 total. Severe depression requires 3 core and ≥8 total. All three require a duration of ≥2 weeks. In addition, the diagnosis of severe depression is allowed with a clinical impression of “severe” in a patient unable/unwilling to describe symptoms in detail. [2] ICD-10 nonpsychotic severe depression requires severe depression without hallucinations/delusions/depressive stupor. ICD-10 psychotic depression requires severe depression plus hallucinations/delusions other than those that are “typically schizophrenic”, or stupor. ICD-10 does not clearly categorize severe depression with only schizophreniform psychotic symptoms; however, such symptoms can occur in severe depression with psychosis (e.g. Tandon R & Greden JF, 1987, PMID 2884810). Moreover, psychotic symptoms can occur in mild/moderate depression (Maj M et al., 2007, PMID 17915981). </div> """.format(CssClass=CssClass) + ICD10_COPYRIGHT_DIV return h
def category(self, req: CamcopsRequest) -> str: totalscore = self.total_score() return (req.wappstring("normal") if totalscore >= 26 else req.wappstring("abnormal"))
def get_task_html(self, req: CamcopsRequest) -> str: vsp = self.score_vsp() naming = self.score_naming() attention = self.score_attention() language = self.score_language() abstraction = self.score_abstraction() memory = self.score_memory() orientation = self.score_orientation() totalscore = self.total_score() category = self.category(req) h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {category} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="69%">Question</th> <th width="31%">Score</th> </tr> """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE), answer(totalscore) + f" / {self.MAX_SCORE}", ), category=tr_qa( self.wxstring(req, "category") + " <sup>[1]</sup>", category), ) h += tr( self.wxstring(req, "subscore_visuospatial"), answer(vsp) + " / 5", tr_class=CssClass.SUBHEADING, ) h += tr( "Path, cube, clock/contour, clock/numbers, clock/hands", ", ".join( answer(x) for x in (self.q1, self.q2, self.q3, self.q4, self.q5)), ) h += tr( self.wxstring(req, "subscore_naming"), answer(naming) + " / 3", tr_class=CssClass.SUBHEADING, ) h += tr( "Lion, rhino, camel", ", ".join(answer(x) for x in (self.q6, self.q7, self.q8)), ) h += tr( self.wxstring(req, "subscore_attention"), answer(attention) + " / 6", tr_class=CssClass.SUBHEADING, ) h += tr( "5 digits forwards, 3 digits backwards, tapping, serial 7s " "[<i>scores 3</i>]", ", ".join( answer(x) for x in (self.q9, self.q10, self.q11, self.q12)), ) h += tr( self.wxstring(req, "subscore_language"), answer(language) + " / 3", tr_class=CssClass.SUBHEADING, ) h += tr( "Repeat sentence 1, repeat sentence 2, fluency to letter ‘F’", ", ".join(answer(x) for x in (self.q13, self.q14, self.q15)), ) h += tr( self.wxstring(req, "subscore_abstraction"), answer(abstraction) + " / 2", tr_class=CssClass.SUBHEADING, ) h += tr( "Means of transportation, measuring instruments", ", ".join(answer(x) for x in (self.q16, self.q17)), ) h += tr( self.wxstring(req, "subscore_memory"), answer(memory) + " / 5", tr_class=CssClass.SUBHEADING, ) h += tr( "Registered on first trial [<i>not scored</i>]", ", ".join( answer(x, formatter_answer=italic) for x in ( self.register_trial1_1, self.register_trial1_2, self.register_trial1_3, self.register_trial1_4, self.register_trial1_5, )), ) h += tr( "Registered on second trial [<i>not scored</i>]", ", ".join( answer(x, formatter_answer=italic) for x in ( self.register_trial2_1, self.register_trial2_2, self.register_trial2_3, self.register_trial2_4, self.register_trial2_5, )), ) h += tr( "Recall FACE, VELVET, CHURCH, DAISY, RED with no cue", ", ".join( answer(x) for x in (self.q18, self.q19, self.q20, self.q21, self.q22)), ) h += tr( "Recall with category cue [<i>not scored</i>]", ", ".join( answer(x, formatter_answer=italic) for x in ( self.recall_category_cue_1, self.recall_category_cue_2, self.recall_category_cue_3, self.recall_category_cue_4, self.recall_category_cue_5, )), ) h += tr( "Recall with multiple-choice cue [<i>not scored</i>]", ", ".join( answer(x, formatter_answer=italic) for x in ( self.recall_mc_cue_1, self.recall_mc_cue_2, self.recall_mc_cue_3, self.recall_mc_cue_4, self.recall_mc_cue_5, )), ) h += tr( self.wxstring(req, "subscore_orientation"), answer(orientation) + " / 6", tr_class=CssClass.SUBHEADING, ) h += tr( "Date, month, year, day, place, city", ", ".join( answer(x) for x in ( self.q23, self.q24, self.q25, self.q26, self.q27, self.q28, )), ) h += subheading_spanning_two_columns(self.wxstring(req, "education_s")) h += tr_qa("≤12 years’ education?", self.education12y_or_less) # noinspection PyTypeChecker h += """ </table> <table class="{CssClass.TASKDETAIL}"> {tr_subhead_images} {tr_images_1} {tr_images_2} </table> <div class="{CssClass.FOOTNOTES}"> [1] Normal is ≥26 (Nasreddine et al. 2005, PubMed ID 15817019). </div> <div class="{CssClass.COPYRIGHT}"> MoCA: Copyright © Ziad Nasreddine. In 2012, could be reproduced without permission for CLINICAL and EDUCATIONAL use (with permission from the copyright holder required for any other use), with no special restrictions on electronic versions. However, as of 2021, electronic versions are prohibited without specific authorization from the copyright holder; see <a href="https://camcops.readthedocs.io/en/latest/tasks/moca.html"> https://camcops.readthedocs.io/en/latest/tasks/moca.html</a>. </div> """.format( CssClass=CssClass, tr_subhead_images=subheading_spanning_two_columns( "Images of tests: trail, cube, clock", th_not_td=True), tr_images_1=tr( td( get_blob_img_html(self.trailpicture), td_class=CssClass.PHOTO, td_width="50%", ), td( get_blob_img_html(self.cubepicture), td_class=CssClass.PHOTO, td_width="50%", ), literal=True, ), tr_images_2=tr( td( get_blob_img_html(self.clockpicture), td_class=CssClass.PHOTO, td_width="50%", ), td("", td_class=CssClass.SUBHEADING), literal=True, ), ) return h
def get_query(self, req: CamcopsRequest) -> SelectBase: which_idnum = req.get_int_param(ViewParam.WHICH_IDNUM, 1) if which_idnum is None: raise exc.HTTPBadRequest(f"{ViewParam.WHICH_IDNUM} not specified") group_ids = req.user.ids_of_groups_user_may_report_on # Step 1: link referral and patient p1 = Patient.__table__.alias("p1") i1 = PatientIdNum.__table__.alias("i1") desc = req.get_id_shortdesc(which_idnum) select_fields = [ CPFTLPSReferral.lps_division, CPFTLPSReferral.referral_date_time, CPFTLPSReferral.referral_priority, p1.c.surname, p1.c.forename, p1.c.dob, i1.c.idnum_value.label(desc), CPFTLPSReferral.patient_location, ] select_from = p1.join( CPFTLPSReferral.__table__, and_( p1.c._current == True, # noqa: E712 CPFTLPSReferral.patient_id == p1.c.id, CPFTLPSReferral._device_id == p1.c._device_id, CPFTLPSReferral._era == p1.c._era, CPFTLPSReferral._current == True, ), ) select_from = select_from.join( i1, and_( i1.c.patient_id == p1.c.id, i1.c._device_id == p1.c._device_id, i1.c._era == p1.c._era, i1.c._current == True, # noqa: E712 ), ) wheres = [i1.c.which_idnum == which_idnum] if not req.user.superuser: # Restrict to accessible groups wheres.append(CPFTLPSReferral._group_id.in_(group_ids)) # Step 2: not yet discharged p2 = Patient.__table__.alias("p2") i2 = PatientIdNum.__table__.alias("i2") discharge = ( select(["*"]).select_from( p2.join( CPFTLPSDischarge.__table__, and_( p2.c._current == True, # noqa: E712 CPFTLPSDischarge.patient_id == p2.c.id, CPFTLPSDischarge._device_id == p2.c._device_id, CPFTLPSDischarge._era == p2.c._era, CPFTLPSDischarge._current == True, ), ).join( i2, and_( i2.c.patient_id == p2.c.id, i2.c._device_id == p2.c._device_id, i2.c._era == p2.c._era, i2.c._current == True, ), )).where( and_( # Link on ID to main query: same patient i2.c.which_idnum == which_idnum, i2.c.idnum_value == i1.c.idnum_value, # Discharge later than referral (CPFTLPSDischarge.discharge_date >= CPFTLPSReferral.referral_date_time), ))) # nopep8 if not req.user.superuser: # Restrict to accessible groups discharge = discharge.where( CPFTLPSDischarge._group_id.in_(group_ids)) wheres.append(~exists(discharge)) # Finish up order_by = [ CPFTLPSReferral.lps_division, CPFTLPSReferral.referral_date_time, CPFTLPSReferral.referral_priority, ] query = (select(select_fields).select_from(select_from).where( and_(*wheres)).order_by(*order_by)) return query
def get_query(self, req: CamcopsRequest) -> SelectBase: which_idnum = req.get_int_param(ViewParam.WHICH_IDNUM, 1) if which_idnum is None: raise exc.HTTPBadRequest("{} not specified".format( ViewParam.WHICH_IDNUM)) group_ids = req.user.ids_of_groups_user_may_report_on # Step 1: link referral and patient # noinspection PyUnresolvedReferences p1 = Patient.__table__.alias("p1") # noinspection PyUnresolvedReferences i1 = PatientIdNum.__table__.alias("i1") desc = req.get_id_shortdesc(which_idnum) select_fields = [ CPFTLPSReferral.lps_division, CPFTLPSReferral.referral_date_time, CPFTLPSReferral.referral_priority, p1.c.surname, p1.c.forename, p1.c.dob, i1.c.idnum_value.label(desc), CPFTLPSReferral.patient_location, ] # noinspection PyUnresolvedReferences select_from = p1.join( CPFTLPSReferral.__table__, and_( p1.c._current == True, CPFTLPSReferral.patient_id == p1.c.id, CPFTLPSReferral._device_id == p1.c._device_id, CPFTLPSReferral._era == p1.c._era, CPFTLPSReferral._current == True, )) # nopep8 select_from = select_from.join(i1, and_( i1.c.patient_id == p1.c.id, i1.c._device_id == p1.c._device_id, i1.c._era == p1.c._era, i1.c._current == True, )) # nopep8 wheres = [ i1.c.which_idnum == which_idnum, ] if not req.user.superuser: # Restrict to accessible groups wheres.append(CPFTLPSReferral._group_id.in_(group_ids)) # Step 2: not yet discharged # noinspection PyUnresolvedReferences p2 = Patient.__table__.alias("p2") # noinspection PyUnresolvedReferences i2 = PatientIdNum.__table__.alias("i2") # noinspection PyUnresolvedReferences discharge = ( select(['*']).select_from( p2.join( CPFTLPSDischarge.__table__, and_( p2.c._current == True, CPFTLPSDischarge.patient_id == p2.c.id, CPFTLPSDischarge._device_id == p2.c._device_id, CPFTLPSDischarge._era == p2.c._era, CPFTLPSDischarge._current == True, )).join( i2, and_( i2.c.patient_id == p2.c.id, i2.c._device_id == p2.c._device_id, i2.c._era == p2.c._era, i2.c._current == True, ))).where( and_( # Link on ID to main query: same patient i2.c.which_idnum == which_idnum, i2.c.idnum_value == i1.c.idnum_value, # Discharge later than referral (CPFTLPSDischarge.discharge_date >= CPFTLPSReferral.referral_date_time), ))) # nopep8 if not req.user.superuser: # Restrict to accessible groups discharge = discharge.where( CPFTLPSDischarge._group_id.in_(group_ids)) wheres.append(~exists(discharge)) # Step 3: not yet clerked # noinspection PyUnresolvedReferences p3 = Patient.__table__.alias("p3") # noinspection PyUnresolvedReferences i3 = PatientIdNum.__table__.alias("i3") # noinspection PyUnresolvedReferences clerking = ( select(['*']).select_from( p3.join( PsychiatricClerking.__table__, and_( p3.c._current == True, PsychiatricClerking.patient_id == p3.c.id, PsychiatricClerking._device_id == p3.c._device_id, PsychiatricClerking._era == p3.c._era, PsychiatricClerking._current == True, )).join( i3, and_( i3.c.patient_id == p3.c.id, i3.c._device_id == p3.c._device_id, i3.c._era == p3.c._era, i3.c._current == True, ))).where( and_( # Link on ID to main query: same patient i3.c.which_idnum == which_idnum, i3.c.idnum_value == i1.c.idnum_value, # Discharge later than referral (PsychiatricClerking.when_created >= CPFTLPSReferral.referral_date_time), ))) # nopep8 if not req.user.superuser: # Restrict to accessible groups clerking = clerking.where( PsychiatricClerking._group_id.in_(group_ids)) wheres.append(~exists(clerking)) # Finish up order_by = [ CPFTLPSReferral.lps_division, CPFTLPSReferral.referral_date_time, CPFTLPSReferral.referral_priority, ] query = (select(select_fields).select_from(select_from).where( and_(*wheres)).order_by(*order_by)) return query
def get_task_html(self, req: CamcopsRequest) -> str: h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {date_pertains_to} {meets_general_criteria} </table> </div> <div class="{CssClass.EXPLANATION}"> {comments} </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), date_pertains_to=tr_qa( req.wappstring("date_pertains_to"), format_datetime(self.date_pertains_to, DateFormat.LONG_DATE, default=None)), meets_general_criteria=tr_qa( self.wxstring(req, "meets_general_criteria") + " <sup>[1]</sup>", # noqa get_true_false_none(req, self.meets_general_criteria())), comments=self.wxstring(req, "comments"), ) h += self.heading_row(req, "core", " <sup>[2]</sup>") for x in Icd10Schizophrenia.A_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "other_positive") for x in Icd10Schizophrenia.B_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "negative_title") for x in Icd10Schizophrenia.C_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "other_criteria") for x in Icd10Schizophrenia.D_NAMES: h += self.row_true_false(req, x) h += self.text_row(req, "duration_comment") for x in Icd10Schizophrenia.E_NAMES: h += self.row_true_false(req, x) h += self.text_row(req, "affective_comment") for x in Icd10Schizophrenia.F_NAMES: h += self.row_true_false(req, x) h += self.heading_row(req, "simple_title") for x in Icd10Schizophrenia.G_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "subtypes") for x in Icd10Schizophrenia.H_NAMES: h += self.row_present_absent(req, x) h += """ </table> <div class="{CssClass.FOOTNOTES}"> [1] All of: (a) at least one core symptom, or at least two of the other positive or negative symptoms; (b) present for a month (etc.); (c) if also manic/depressed, schizophreniform psychosis came first; (d) not attributable to organic brain disease or psychoactive substances. [2] Symptom definitions from: (a) Oyebode F (2008). Sims’ Symptoms in the Mind: An Introduction to Descriptive Psychopathology. Fourth edition, Saunders, Elsevier, Edinburgh. (b) Pawar AV & Spence SA (2003), PMID 14519605. </div> """.format(CssClass=CssClass) + ICD10_COPYRIGHT_DIV return h
def get_task_html(self, req: CamcopsRequest) -> str: if self.modality == MODALITY_AUDITORY: modality = req.wappstring("auditory") elif self.modality == MODALITY_VISUAL: modality = req.wappstring("visual") else: modality = None h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} </table> </div> <div class="{CssClass.EXPLANATION}"> The ExpDet-Threshold task measures visual and auditory thresholds for stimuli on a noisy background, using a single-interval up/down method. It is intended as a prequel to the Expectation–Detection task. </div> <table class="{CssClass.TASKCONFIG}"> <tr> <th width="50%">Configuration variable</th> <th width="50%">Value</th> </tr> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), ) h += tr_qa("Modality", modality) h += tr_qa("Target number", self.target_number) h += tr_qa("Background filename", ws.webify(self.background_filename)) h += tr_qa("Background intensity", self.background_intensity) h += tr_qa("Target filename", ws.webify(self.target_filename)) h += tr_qa("(For visual targets) Target duration (s)", self.visual_target_duration_s) h += tr_qa("Start intensity (minimum)", self.start_intensity_min) h += tr_qa("Start intensity (maximum)", self.start_intensity_max) h += tr_qa("Initial (large) intensity step", self.initial_large_intensity_step) h += tr_qa("Main (small) intensity step", self.main_small_intensity_step) h += tr_qa("Number of trials in main sequence", self.num_trials_in_main_sequence) h += tr_qa("Probability of a catch trial", self.p_catch_trial) h += tr_qa("Prompt", self.prompt) h += tr_qa("Intertrial interval (ITI) (s)", self.iti_s) h += """ </table> <table class="{CssClass.TASKDETAIL}"> <tr><th width="50%">Measure</th><th width="50%">Value</th></tr> """.format(CssClass=CssClass) h += tr_qa("Finished?", get_yes_no_none(req, self.finished)) h += tr_qa("Logistic intercept", ws.number_to_dp(self.intercept, DP)) h += tr_qa("Logistic slope", ws.number_to_dp(self.slope, DP)) h += tr_qa("Logistic k (= slope)", ws.number_to_dp(self.k, DP)) h += tr_qa("Logistic theta (= –intercept/slope)", ws.number_to_dp(self.theta, DP)) h += tr_qa("Intensity for {}% detection".format(100 * LOWER_MARKER), ws.number_to_dp(self.logistic_x_from_p(LOWER_MARKER), DP)) h += tr_qa("Intensity for 50% detection", ws.number_to_dp(self.theta, DP)) h += tr_qa("Intensity for {}% detection".format(100 * UPPER_MARKER), ws.number_to_dp(self.logistic_x_from_p(UPPER_MARKER), DP)) h += """ </table> """ h += self.get_trial_html(req) return h
def get_task_html(self, req: CamcopsRequest) -> str: normal_score_dict = { None: None, 1: "1 — " + self.wxstring(req, "a0"), 2: "2 — " + self.wxstring(req, "a1"), 3: "3 — " + self.wxstring(req, "a2"), 4: "4 — " + self.wxstring(req, "a3"), } reverse_score_dict = { None: None, 4: "4 — " + self.wxstring(req, "a0"), 3: "3 — " + self.wxstring(req, "a1"), 2: "2 — " + self.wxstring(req, "a2"), 1: "1 — " + self.wxstring(req, "a3"), } reverse_q_nums = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20} 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) score_dict = normal_score_dict if q_num in reverse_q_nums: score_dict = reverse_score_dict answer_cell = get_from_dict(score_dict, score) rows += tr_qa(question_cell, answer_cell) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {negative_urgency_score} {lack_of_perseverance_score} {lack_of_premeditation_score} {sensation_seeking_score} {positive_urgency_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Score</th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Sum for questions 1–20. [2] Sum for questions 6, 8, 13, 15. [3] Sum for questions 1, 4, 7, 11. [4] Sum for questions 2, 5, 12, 19. [5] Sum for questions 9, 14, 16, 18. [6] Sum for questions 3, 10, 17, 20. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>", f"{answer(self.total_score())} {fullscale_range}", ), negative_urgency_score=tr( self.wxstring(req, "negative_urgency") + " <sup>[2]</sup>", f"{answer(self.negative_urgency_score())} {subscale_range}", ), lack_of_perseverance_score=tr( self.wxstring(req, "lack_of_perseverance") + " <sup>[3]</sup>", f"{answer(self.lack_of_perseverance_score())} {subscale_range}", # noqa: E501 ), lack_of_premeditation_score=tr( self.wxstring(req, "lack_of_premeditation") + " <sup>[4]</sup>", f"{answer(self.lack_of_premeditation_score())} {subscale_range}", # noqa: E501 ), sensation_seeking_score=tr( self.wxstring(req, "sensation_seeking") + " <sup>[5]</sup>", f"{answer(self.sensation_seeking_score())} {subscale_range}", ), positive_urgency_score=tr( self.wxstring(req, "positive_urgency") + " <sup>[6]</sup>", f"{answer(self.positive_urgency_score())} {subscale_range}", ), rows=rows, ) return html
def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.HONOSWA_PROCEDURE_ASSESSMENT)) ] # noqa if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.HONOSWA_SCALE), { req.snomed(SnomedLookup.HONOSWA_SCORE): self.total_score(), req.snomed(SnomedLookup.HONOSWA_1_OVERACTIVE_SCORE): self.q1, # noqa req.snomed(SnomedLookup.HONOSWA_2_SELFINJURY_SCORE): self.q2, # noqa req.snomed(SnomedLookup.HONOSWA_3_SUBSTANCE_SCORE): self.q3, # noqa req.snomed(SnomedLookup.HONOSWA_4_COGNITIVE_SCORE): self.q4, # noqa req.snomed(SnomedLookup.HONOSWA_5_PHYSICAL_SCORE): self.q5, req.snomed(SnomedLookup.HONOSWA_6_PSYCHOSIS_SCORE): self.q6, # noqa req.snomed(SnomedLookup.HONOSWA_7_DEPRESSION_SCORE): self.q7, # noqa req.snomed(SnomedLookup.HONOSWA_8_OTHERMENTAL_SCORE): self.q8, # noqa req.snomed(SnomedLookup.HONOSWA_9_RELATIONSHIPS_SCORE): self.q9, # noqa req.snomed(SnomedLookup.HONOSWA_10_ADL_SCORE): self.q10, req.snomed(SnomedLookup.HONOSWA_11_LIVINGCONDITIONS_SCORE): self.q11, # noqa req.snomed(SnomedLookup.HONOSWA_12_OCCUPATION_SCORE): self.q12, # noqa })) return codes
def get_task_html(self, req: CamcopsRequest) -> str: main_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") } q10_dict = { None: None, 0: "0 — " + self.wxstring(req, "fa0"), 1: "1 — " + self.wxstring(req, "fa1"), 2: "2 — " + self.wxstring(req, "fa2"), 3: "3 — " + self.wxstring(req, "fa3") } q_a = "" for i in range(1, self.N_MAIN_QUESTIONS + 1): nstr = str(i) q_a += tr_qa(self.wxstring(req, "q" + nstr), get_from_dict(main_dict, getattr(self, "q" + nstr))) q_a += tr_qa("10. " + self.wxstring(req, "finalq"), get_from_dict(q10_dict, self.q10)) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {depression_severity} {n_symptoms} {mds} {ods} </table> </div> <div class="{CssClass.EXPLANATION}"> Ratings are over the last 2 weeks. </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] Sum for questions 1–9. [2] Total score ≥20 severe, ≥15 moderately severe, ≥10 moderate, ≥5 mild, <5 none. [3] Number of questions 1–2 rated ≥2. [4] Number of questions 3–8 rated ≥2, or question 9 rated ≥1. [5] ≥1 core symptom and ≥5 total symptoms (as per DSM-IV-TR page 356). [6] ≥1 core symptom and 2–4 total symptoms (as per DSM-IV-TR page 775). </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.wappstring("total_score") + " <sup>[1]</sup>", answer(self.total_score()) + " / {}".format(self.MAX_SCORE_MAIN)), depression_severity=tr_qa( self.wxstring(req, "depression_severity") + " <sup>[2]</sup>", self.severity(req)), n_symptoms=tr( "Number of symptoms: core <sup>[3]</sup>, other " "<sup>[4]</sup>, total", answer(self.n_core()) + "/2, " + answer(self.n_other()) + "/7, " + answer(self.n_total()) + "/9"), mds=tr_qa( self.wxstring(req, "mds") + " <sup>[5]</sup>", get_yes_no(req, self.is_mds())), ods=tr_qa( self.wxstring(req, "ods") + " <sup>[6]</sup>", get_yes_no(req, self.is_ods())), q_a=q_a, ) return h
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_task_html(self, req: CamcopsRequest) -> str: main_dict = { None: None, 0: "0 — " + self.wxstring(req, "old_option0"), 1: "1 — " + self.wxstring(req, "old_option1"), 2: "2 — " + self.wxstring(req, "old_option2"), 3: "3 — " + self.wxstring(req, "old_option3"), 4: "4 — " + self.wxstring(req, "old_option4"), 5: "5 — " + self.wxstring(req, "old_option5"), 6: "6 — " + self.wxstring(req, "old_option6"), 7: "7 — " + self.wxstring(req, "old_option7") } q19_dict = { None: None, 1: self.wxstring(req, "q19_option1"), 2: self.wxstring(req, "q19_option2"), 3: self.wxstring(req, "q19_option3"), 4: self.wxstring(req, "q19_option4"), 5: self.wxstring(req, "q19_option5"), 6: self.wxstring(req, "q19_option6"), 7: self.wxstring(req, "q19_option7") } q20_dict = { None: None, 0: self.wxstring(req, "q20_option0"), 1: self.wxstring(req, "q20_option1"), 2: self.wxstring(req, "q20_option2"), 3: self.wxstring(req, "q20_option3"), 4: self.wxstring(req, "q20_option4"), 5: self.wxstring(req, "q20_option5"), 6: self.wxstring(req, "q20_option6"), 7: self.wxstring(req, "q20_option7") } q_a = "" for i in range(1, Bprs.NQUESTIONS - 1): # only does 1-18 q_a += tr_qa(self.wxstring(req, "q" + str(i) + "_title"), get_from_dict(main_dict, getattr(self, "q" + str(i)))) q_a += tr_qa(self.wxstring(req, "q19_title"), get_from_dict(q19_dict, self.q19)) q_a += tr_qa(self.wxstring(req, "q20_title"), get_from_dict(q20_dict, self.q20)) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} </table> </div> <div class="{CssClass.EXPLANATION}"> Ratings pertain to the past week, or behaviour during interview. Each question has specific answer definitions (see e.g. tablet app). </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer <sup>[2]</sup></th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> [1] Only questions 1–18 are scored. [2] All answers are in the range 1–7, or 0 (not assessed, for some). </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.wappstring("total_score") + " (0–{maxscore}; 18–{maxscore} if all rated) " "<sup>[1]</sup>".format(maxscore=self.MAX_SCORE), answer(self.total_score())), q_a=q_a, ) return h
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
def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [SnomedExpression(req.snomed(SnomedLookup.BPRS1962_SCALE))] return codes
def get_diagnosis_inc_exc_report_query( req: CamcopsRequest, diagnosis_class: Type[DiagnosisBase], item_class: Type[DiagnosisItemBase], item_fk_fieldname: str, system: str, which_idnum: int, inclusion_dx: List[str], exclusion_dx: List[str], age_minimum_y: int, age_maximum_y: int, ) -> SelectBase: """ As for get_diagnosis_report_query, but this makes some modifications to do inclusion and exclusion criteria. - We need a linking number to perform exclusion criteria. - Therefore, we use a single ID number, which must not be NULL. """ # The basics: desc = req.get_id_desc(which_idnum) or "BAD_IDNUM" # noinspection PyUnresolvedReferences select_fields = [ Patient.surname.label("surname"), Patient.forename.label("forename"), Patient.dob.label("dob"), Patient.sex.label("sex"), PatientIdNum.idnum_value.label(desc), diagnosis_class.when_created.label("when_created"), literal(system).label("system"), item_class.code.label("code"), item_class.description.label("description"), ] # noinspection PyUnresolvedReferences select_from = ( Patient.__table__.join( diagnosis_class.__table__, and_( diagnosis_class.patient_id == Patient.id, diagnosis_class._device_id == Patient._device_id, diagnosis_class._era == Patient._era, diagnosis_class._current == True, # noqa: E712 ), ).join( item_class.__table__, and_( getattr(item_class, item_fk_fieldname) == diagnosis_class.id, item_class._device_id == diagnosis_class._device_id, item_class._era == diagnosis_class._era, item_class._current == True, ), ).join( PatientIdNum.__table__, and_( PatientIdNum.patient_id == Patient.id, PatientIdNum._device_id == Patient._device_id, PatientIdNum._era == Patient._era, PatientIdNum._current == True, PatientIdNum.which_idnum == which_idnum, PatientIdNum.idnum_value.isnot(None), # NOT NULL ), )) wheres = [Patient._current == True] # noqa: E712 if not req.user.superuser: # Restrict to accessible groups group_ids = req.user.ids_of_groups_user_may_report_on wheres.append(diagnosis_class._group_id.in_(group_ids)) else: group_ids = [] # type: List[int] # to stop type-checker moaning below # Age limits are simple, as the same patient has the same age for # all diagnosis rows. today = req.today if age_maximum_y is not None: # Example: max age is 40; earliest (oldest) DOB is therefore 41 # years ago plus one day (e.g. if it's 15 June 2010, then earliest # DOB is 16 June 1969; a person born then will be 41 tomorrow). earliest_dob = pendulum_date_to_datetime_date( today.subtract(years=age_maximum_y + 1).add(days=1)) wheres.append(Patient.dob >= earliest_dob) if age_minimum_y is not None: # Example: min age is 20; latest (youngest) DOB is therefore 20 # years ago (e.g. if it's 15 June 2010, latest DOB is 15 June 1990; # if you're born after that, you're not 20 yet). latest_dob = pendulum_date_to_datetime_date( today.subtract(years=age_minimum_y)) wheres.append(Patient.dob <= latest_dob) # Diagnosis criteria are a little bit more complex. # # We can reasonably do inclusion criteria as "show the diagnoses # matching the inclusion criteria" (not the more complex "show all # diagnoses for patients having at least one inclusion diagnosis", # which is likely to be too verbose for patient finding). inclusion_criteria = [] # type: List[ColumnElement] for idx in inclusion_dx: inclusion_criteria.append(item_class.code.like(idx)) wheres.append(or_(*inclusion_criteria)) # Exclusion criteria are the trickier: we need to be able to link # multiple diagnoses for the same patient, so we need to use a linking # ID number. if exclusion_dx: # noinspection PyUnresolvedReferences edx_items = item_class.__table__.alias("edx_items") # noinspection PyUnresolvedReferences edx_sets = diagnosis_class.__table__.alias("edx_sets") # noinspection PyUnresolvedReferences edx_patient = Patient.__table__.alias("edx_patient") # noinspection PyUnresolvedReferences edx_idnum = PatientIdNum.__table__.alias("edx_idnum") edx_joined = ( edx_items.join( edx_sets, and_( getattr(edx_items.c, item_fk_fieldname) == edx_sets.c.id, edx_items.c._device_id == edx_sets.c._device_id, edx_items.c._era == edx_sets.c._era, edx_items.c._current == True, # noqa: E712 ), ).join( edx_patient, and_( edx_sets.c.patient_id == edx_patient.c.id, edx_sets.c._device_id == edx_patient.c._device_id, edx_sets.c._era == edx_patient.c._era, edx_sets.c._current == True, # noqa: E712 ), ).join( edx_idnum, and_( edx_idnum.c.patient_id == edx_patient.c.id, edx_idnum.c._device_id == edx_patient.c._device_id, edx_idnum.c._era == edx_patient.c._era, edx_idnum.c._current == True, # noqa: E712 edx_idnum.c.which_idnum == which_idnum, ), )) exclusion_criteria = [] # type: List[ColumnElement] for edx in exclusion_dx: exclusion_criteria.append(edx_items.c.code.like(edx)) edx_wheres = [ edx_items.c._current == True, # noqa: E712 edx_idnum.c.idnum_value == PatientIdNum.idnum_value, or_(*exclusion_criteria), ] # Note the join above between the main and the EXISTS clauses. # We don't use an alias for the main copy of the PatientIdNum table, # and we do for the EXISTS version. This is fine; e.g. # https://msdn.microsoft.com/en-us/library/ethytz2x.aspx example: # SELECT boss.name, employee.name # FROM employee # INNER JOIN employee boss ON employee.manager_id = boss.emp_id; if not req.user.superuser: # Restrict to accessible groups # group_ids already defined from above edx_wheres.append(edx_sets.c._group_id.in_(group_ids)) # ... bugfix 2018-06-19: "wheres" -> "edx_wheres" exclusion_select = (select(["*"]).select_from(edx_joined).where( and_(*edx_wheres))) wheres.append(not_(exists(exclusion_select))) query = select(select_fields).select_from(select_from).where(and_(*wheres)) return query
def category(self, req: CamcopsRequest) -> str: totalscore = self.total_score() return (req.sstring(SS.NORMAL) if totalscore >= 26 else req.sstring(SS.ABNORMAL))
def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: if not self.is_complete(): return [] return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]
def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.PSYCHIATRIC_ASSESSMENT_PROCEDURE)) ] # noqa return codes