Coverage for tasks/honos.py: 51%

176 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/honos.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

11 CamCOPS is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CamCOPS is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26""" 

27 

28from typing import Any, List, Optional, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.orm import Mapped, mapped_column 

32from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

33 

34from camcops_server.cc_modules.cc_constants import CssClass 

35from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

37from camcops_server.cc_modules.cc_html import ( 

38 answer, 

39 subheading_spanning_two_columns, 

40 tr, 

41 tr_qa, 

42) 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 mapped_camcops_column, 

47 CharColType, 

48 PermittedValueChecker, 

49) 

50from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

51from camcops_server.cc_modules.cc_task import ( 

52 get_from_dict, 

53 Task, 

54 TaskHasClinicianMixin, 

55 TaskHasPatientMixin, 

56) 

57from camcops_server.cc_modules.cc_text import SS 

58from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

59 

60 

61PV_MAIN = [0, 1, 2, 3, 4, 9] 

62PV_PROBLEMTYPE = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] 

63 

64FOOTNOTE_SCORING = """ 

65 [1] 0 = no problem; 

66 1 = minor problem requiring no action; 

67 2 = mild problem but definitely present; 

68 3 = moderately severe problem; 

69 4 = severe to very severe problem; 

70 9 = not known. 

71""" 

72 

73 

74# ============================================================================= 

75# HoNOS abstract base class 

76# ============================================================================= 

77 

78 

79# noinspection PyAbstractClass 

80class HonosBase(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501 

81 __abstract__ = True 

82 provides_trackers = True 

83 

84 period_rated: Mapped[Optional[str]] = mapped_column( 

85 UnicodeText, comment="Period being rated" 

86 ) 

87 

88 COPYRIGHT_DIV = f""" 

89 <div class="{CssClass.COPYRIGHT}"> 

90 Health of the Nation Outcome Scales: 

91 Copyright © Royal College of Psychiatrists. 

92 Used here with permission. 

93 </div> 

94 """ 

95 

96 QFIELDS = None # type: List[str] # must be overridden 

97 MAX_SCORE = None # type: int # must be overridden 

98 

99 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

100 return [ 

101 TrackerInfo( 

102 value=self.total_score(), 

103 plot_label=f"{self.shortname} total score", 

104 axis_label=f"Total score (out of {self.MAX_SCORE})", 

105 axis_min=-0.5, 

106 axis_max=self.MAX_SCORE + 0.5, 

107 ) 

108 ] 

109 

110 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

111 if not self.is_complete(): 

112 return CTV_INCOMPLETE 

113 return [ 

114 CtvInfo( 

115 content=( 

116 f"{self.shortname} total score " 

117 f"{self.total_score()}/{self.MAX_SCORE}" 

118 ) 

119 ) 

120 ] 

121 

122 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

123 return self.standard_task_summary_fields() + [ 

124 SummaryElement( 

125 name="total", 

126 coltype=Integer(), 

127 value=self.total_score(), 

128 comment=f"Total score (/{self.MAX_SCORE})", 

129 ) 

130 ] 

131 

132 def _total_score_for_fields(self, fieldnames: List[str]) -> int: 

133 total = 0 

134 for qname in fieldnames: 

135 value = getattr(self, qname) 

136 if value is not None and 0 <= value <= 4: 

137 # i.e. ignore null values and 9 (= not known) 

138 total += value 

139 return total 

140 

141 def total_score(self) -> int: 

142 return self._total_score_for_fields(self.QFIELDS) 

143 

144 def get_q(self, req: CamcopsRequest, q: int) -> str: 

145 return self.wxstring(req, "q" + str(q) + "_s") 

146 

147 def get_answer(self, req: CamcopsRequest, q: int, a: int) -> Optional[str]: 

148 if a == 9: 

149 return self.wxstring(req, "option9") 

150 if a is None or a < 0 or a > 4: 

151 return None 

152 return self.wxstring(req, "q" + str(q) + "_option" + str(a)) 

153 

154 

155# ============================================================================= 

156# HoNOS 

157# ============================================================================= 

158 

159 

160class Honos( 

161 HonosBase, 

162): 

163 """ 

164 Server implementation of the HoNOS task. 

165 """ 

166 

167 __tablename__ = "honos" 

168 shortname = "HoNOS" 

169 info_filename_stem = "honos" 

170 

171 @classmethod 

172 def extend_columns(cls: Type["Honos"], **kwargs: Any) -> None: 

173 add_multiple_columns( 

174 cls, 

175 "q", 

176 1, 

177 cls.NQUESTIONS, 

178 pv=PV_MAIN, 

179 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

180 comment_strings=[ 

181 "overactive/aggressive/disruptive/agitated", 

182 "deliberate self-harm", 

183 "problem-drinking/drug-taking", 

184 "cognitive problems", 

185 "physical illness/disability", 

186 "hallucinations/delusions", 

187 "depressed mood", 

188 "other mental/behavioural problem", 

189 "relationship problems", 

190 "activities of daily living", 

191 "problems with living conditions", 

192 "occupation/activities", 

193 ], 

194 ) 

195 

196 q8problemtype: Mapped[Optional[str]] = mapped_camcops_column( 

197 CharColType, 

198 permitted_value_checker=PermittedValueChecker( 

199 permitted_values=PV_PROBLEMTYPE 

200 ), 

201 comment="Q8: type of problem (A phobic; B anxiety; " 

202 "C obsessive-compulsive; D mental strain/tension; " 

203 "E dissociative; F somatoform; G eating; H sleep; " 

204 "I sexual; J other, specify)", 

205 ) 

206 q8otherproblem: Mapped[Optional[str]] = mapped_column( 

207 UnicodeText, comment="Q8: other problem: specify" 

208 ) 

209 

210 NQUESTIONS = 12 

211 QFIELDS = strseq("q", 1, NQUESTIONS) 

212 MAX_SCORE = 48 

213 

214 @staticmethod 

215 def longname(req: "CamcopsRequest") -> str: 

216 _ = req.gettext 

217 return _("Health of the Nation Outcome Scales, working age adults") 

218 

219 # noinspection PyUnresolvedReferences 

220 def is_complete(self) -> bool: 

221 if self.any_fields_none(self.QFIELDS): 

222 return False 

223 if not self.field_contents_valid(): 

224 return False 

225 if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: # type: ignore[attr-defined] # noqa: E501 

226 return False 

227 if ( 

228 self.q8 != 0 # type: ignore[attr-defined] 

229 and self.q8 != 9 # type: ignore[attr-defined] 

230 and self.q8problemtype == "J" 

231 and self.q8otherproblem is None 

232 ): 

233 return False 

234 return self.period_rated is not None 

235 

236 def get_task_html(self, req: CamcopsRequest) -> str: 

237 q8_problem_type_dict = { 

238 None: None, 

239 "A": self.wxstring(req, "q8problemtype_option_a"), 

240 "B": self.wxstring(req, "q8problemtype_option_b"), 

241 "C": self.wxstring(req, "q8problemtype_option_c"), 

242 "D": self.wxstring(req, "q8problemtype_option_d"), 

243 "E": self.wxstring(req, "q8problemtype_option_e"), 

244 "F": self.wxstring(req, "q8problemtype_option_f"), 

245 "G": self.wxstring(req, "q8problemtype_option_g"), 

246 "H": self.wxstring(req, "q8problemtype_option_h"), 

247 "I": self.wxstring(req, "q8problemtype_option_i"), 

248 "J": self.wxstring(req, "q8problemtype_option_j"), 

249 } 

250 one_to_eight = "" 

251 for i in range(1, 8 + 1): 

252 one_to_eight += tr_qa( 

253 self.get_q(req, i), 

254 self.get_answer(req, i, getattr(self, "q" + str(i))), 

255 ) 

256 nine_onwards = "" 

257 for i in range(9, self.NQUESTIONS + 1): 

258 nine_onwards += tr_qa( 

259 self.get_q(req, i), 

260 self.get_answer(req, i, getattr(self, "q" + str(i))), 

261 ) 

262 

263 h = """ 

264 <div class="{CssClass.SUMMARY}"> 

265 <table class="{CssClass.SUMMARY}"> 

266 {tr_is_complete} 

267 {total_score} 

268 </table> 

269 </div> 

270 <table class="{CssClass.TASKDETAIL}"> 

271 <tr> 

272 <th width="50%">Question</th> 

273 <th width="50%">Answer <sup>[1]</sup></th> 

274 </tr> 

275 {period_rated} 

276 {one_to_eight} 

277 {q8problemtype} 

278 {q8otherproblem} 

279 {nine_onwards} 

280 </table> 

281 <div class="{CssClass.FOOTNOTES}"> 

282 {FOOTNOTE_SCORING} 

283 </div> 

284 {copyright_div} 

285 """.format( 

286 CssClass=CssClass, 

287 tr_is_complete=self.get_is_complete_tr(req), 

288 total_score=tr( 

289 req.sstring(SS.TOTAL_SCORE), 

290 answer(self.total_score()) + f" / {self.MAX_SCORE}", 

291 ), 

292 period_rated=tr_qa( 

293 self.wxstring(req, "period_rated"), self.period_rated 

294 ), 

295 one_to_eight=one_to_eight, 

296 q8problemtype=tr_qa( 

297 self.wxstring(req, "q8problemtype_s"), 

298 get_from_dict(q8_problem_type_dict, self.q8problemtype), 

299 ), 

300 q8otherproblem=tr_qa( 

301 self.wxstring(req, "q8otherproblem_s"), self.q8otherproblem 

302 ), 

303 nine_onwards=nine_onwards, 

304 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

305 copyright_div=self.COPYRIGHT_DIV, 

306 ) 

307 return h 

308 

309 # noinspection PyUnresolvedReferences 

310 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

311 codes = [ 

312 SnomedExpression( 

313 req.snomed(SnomedLookup.HONOSWA_PROCEDURE_ASSESSMENT) 

314 ) 

315 ] 

316 if self.is_complete(): 

317 codes.append( 

318 SnomedExpression( 

319 req.snomed(SnomedLookup.HONOSWA_SCALE), 

320 { 

321 req.snomed( 

322 SnomedLookup.HONOSWA_SCORE 

323 ): self.total_score(), 

324 req.snomed( 

325 SnomedLookup.HONOSWA_1_OVERACTIVE_SCORE 

326 ): self.q1, # type: ignore[attr-defined] 

327 req.snomed( 

328 SnomedLookup.HONOSWA_2_SELFINJURY_SCORE 

329 ): self.q2, # type: ignore[attr-defined] 

330 req.snomed( 

331 SnomedLookup.HONOSWA_3_SUBSTANCE_SCORE 

332 ): self.q3, # type: ignore[attr-defined] 

333 req.snomed( 

334 SnomedLookup.HONOSWA_4_COGNITIVE_SCORE 

335 ): self.q4, # type: ignore[attr-defined] 

336 req.snomed( 

337 SnomedLookup.HONOSWA_5_PHYSICAL_SCORE 

338 ): self.q5, # type: ignore[attr-defined] 

339 req.snomed( 

340 SnomedLookup.HONOSWA_6_PSYCHOSIS_SCORE 

341 ): self.q6, # type: ignore[attr-defined] 

342 req.snomed( 

343 SnomedLookup.HONOSWA_7_DEPRESSION_SCORE 

344 ): self.q7, # type: ignore[attr-defined] 

345 req.snomed( 

346 SnomedLookup.HONOSWA_8_OTHERMENTAL_SCORE 

347 ): self.q8, # type: ignore[attr-defined] 

348 req.snomed( 

349 SnomedLookup.HONOSWA_9_RELATIONSHIPS_SCORE 

350 ): self.q9, # type: ignore[attr-defined] 

351 req.snomed( 

352 SnomedLookup.HONOSWA_10_ADL_SCORE 

353 ): self.q10, # type: ignore[attr-defined] 

354 req.snomed( 

355 SnomedLookup.HONOSWA_11_LIVINGCONDITIONS_SCORE 

356 ): self.q11, # type: ignore[attr-defined] 

357 req.snomed( 

358 SnomedLookup.HONOSWA_12_OCCUPATION_SCORE 

359 ): self.q12, # type: ignore[attr-defined] 

360 }, 

361 ) 

362 ) 

363 return codes 

364 

365 

366# ============================================================================= 

367# HoNOS 65+ 

368# ============================================================================= 

369 

370 

371class Honos65( 

372 HonosBase, 

373): 

374 """ 

375 Server implementation of the HoNOS 65+ task. 

376 """ 

377 

378 __tablename__ = "honos65" 

379 shortname = "HoNOS 65+" 

380 info_filename_stem = "honos" 

381 

382 @classmethod 

383 def extend_columns(cls: Type["Honos65"], **kwargs: Any) -> None: 

384 add_multiple_columns( 

385 cls, 

386 "q", 

387 1, 

388 cls.NQUESTIONS, 

389 pv=PV_MAIN, 

390 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

391 comment_strings=[ # not exactly identical to HoNOS 

392 "behavioural disturbance", 

393 "deliberate self-harm", 

394 "problem drinking/drug-taking", 

395 "cognitive problems", 

396 "physical illness/disability", 

397 "hallucinations/delusions", 

398 "depressive symptoms", 

399 "other mental/behavioural problem", 

400 "relationship problems", 

401 "activities of daily living", 

402 "living conditions", 

403 "occupation/activities", 

404 ], 

405 ) 

406 

407 q8problemtype: Mapped[Optional[str]] = mapped_camcops_column( 

408 CharColType, 

409 permitted_value_checker=PermittedValueChecker( 

410 permitted_values=PV_PROBLEMTYPE 

411 ), 

412 comment="Q8: type of problem (A phobic; B anxiety; " 

413 "C obsessive-compulsive; D stress; " # NB slight difference: D 

414 "E dissociative; F somatoform; G eating; H sleep; " 

415 "I sexual; J other, specify)", 

416 ) 

417 q8otherproblem: Mapped[Optional[str]] = mapped_column( 

418 UnicodeText, comment="Q8: other problem: specify" 

419 ) 

420 

421 NQUESTIONS = 12 

422 QFIELDS = strseq("q", 1, NQUESTIONS) 

423 MAX_SCORE = 48 

424 

425 @staticmethod 

426 def longname(req: "CamcopsRequest") -> str: 

427 _ = req.gettext 

428 return _("Health of the Nation Outcome Scales, older adults") 

429 

430 # noinspection PyUnresolvedReferences 

431 def is_complete(self) -> bool: 

432 if self.any_fields_none(self.QFIELDS): 

433 return False 

434 if not self.field_contents_valid(): 

435 return False 

436 if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: # type: ignore[attr-defined] # noqa: E501 

437 return False 

438 if ( 

439 self.q8 != 0 # type: ignore[attr-defined] 

440 and self.q8 != 9 # type: ignore[attr-defined] 

441 and self.q8problemtype == "J" 

442 and self.q8otherproblem is None 

443 ): 

444 return False 

445 return self.period_rated is not None 

446 

447 def get_task_html(self, req: CamcopsRequest) -> str: 

448 q8_problem_type_dict = { 

449 None: None, 

450 "A": self.wxstring(req, "q8problemtype_option_a"), 

451 "B": self.wxstring(req, "q8problemtype_option_b"), 

452 "C": self.wxstring(req, "q8problemtype_option_c"), 

453 "D": self.wxstring(req, "q8problemtype_option_d"), 

454 "E": self.wxstring(req, "q8problemtype_option_e"), 

455 "F": self.wxstring(req, "q8problemtype_option_f"), 

456 "G": self.wxstring(req, "q8problemtype_option_g"), 

457 "H": self.wxstring(req, "q8problemtype_option_h"), 

458 "I": self.wxstring(req, "q8problemtype_option_i"), 

459 "J": self.wxstring(req, "q8problemtype_option_j"), 

460 } 

461 one_to_eight = "" 

462 for i in range(1, 8 + 1): 

463 one_to_eight += tr_qa( 

464 self.get_q(req, i), 

465 self.get_answer(req, i, getattr(self, "q" + str(i))), 

466 ) 

467 nine_onwards = "" 

468 for i in range(9, Honos.NQUESTIONS + 1): 

469 nine_onwards += tr_qa( 

470 self.get_q(req, i), 

471 self.get_answer(req, i, getattr(self, "q" + str(i))), 

472 ) 

473 

474 h = """ 

475 <div class="{CssClass.SUMMARY}"> 

476 <table class="{CssClass.SUMMARY}"> 

477 {tr_is_complete} 

478 {total_score} 

479 </table> 

480 </div> 

481 <table class="{CssClass.TASKDETAIL}"> 

482 <tr> 

483 <th width="50%">Question</th> 

484 <th width="50%">Answer <sup>[1]</sup></th> 

485 </tr> 

486 {period_rated} 

487 {one_to_eight} 

488 {q8problemtype} 

489 {q8otherproblem} 

490 {nine_onwards} 

491 </table> 

492 <div class="{CssClass.FOOTNOTES}"> 

493 {FOOTNOTE_SCORING} 

494 </div> 

495 {copyright_div} 

496 """.format( 

497 CssClass=CssClass, 

498 tr_is_complete=self.get_is_complete_tr(req), 

499 total_score=tr( 

500 req.sstring(SS.TOTAL_SCORE), 

501 answer(self.total_score()) + f" / {self.MAX_SCORE}", 

502 ), 

503 period_rated=tr_qa( 

504 self.wxstring(req, "period_rated"), self.period_rated 

505 ), 

506 one_to_eight=one_to_eight, 

507 q8problemtype=tr_qa( 

508 self.wxstring(req, "q8problemtype_s"), 

509 get_from_dict(q8_problem_type_dict, self.q8problemtype), 

510 ), 

511 q8otherproblem=tr_qa( 

512 self.wxstring(req, "q8otherproblem_s"), self.q8otherproblem 

513 ), 

514 nine_onwards=nine_onwards, 

515 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

516 copyright_div=self.COPYRIGHT_DIV, 

517 ) 

518 return h 

519 

520 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

521 codes = [ 

522 SnomedExpression( 

523 req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT) 

524 ) 

525 ] 

526 if self.is_complete(): 

527 codes.append( 

528 SnomedExpression( 

529 req.snomed(SnomedLookup.HONOS65_SCALE), 

530 { 

531 req.snomed( 

532 SnomedLookup.HONOS65_SCORE 

533 ): self.total_score() 

534 }, 

535 ) 

536 ) 

537 return codes 

538 

539 

540# ============================================================================= 

541# HoNOSCA 

542# ============================================================================= 

543 

544 

545class Honosca( 

546 HonosBase, 

547): 

548 """ 

549 Server implementation of the HoNOSCA task. 

550 """ 

551 

552 __tablename__ = "honosca" 

553 shortname = "HoNOSCA" 

554 info_filename_stem = "honos" 

555 

556 NQUESTIONS = 15 

557 

558 @classmethod 

559 def extend_columns(cls: Type["Honosca"], **kwargs: Any) -> None: 

560 add_multiple_columns( 

561 cls, 

562 "q", 

563 1, 

564 cls.NQUESTIONS, 

565 pv=PV_MAIN, 

566 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

567 comment_strings=[ 

568 "disruptive/antisocial/aggressive", 

569 "overactive/inattentive", 

570 "self-harm", 

571 "alcohol/drug misuse", 

572 "scholastic/language problems", 

573 "physical illness/disability", 

574 "delusions/hallucinations", 

575 "non-organic somatic symptoms", 

576 "emotional symptoms", 

577 "peer relationships", 

578 "self-care and independence", 

579 "family life/relationships", 

580 "school attendance", 

581 "problems with knowledge/understanding of child's problems", 

582 "lack of information about services", 

583 ], 

584 ) 

585 

586 QFIELDS = strseq("q", 1, NQUESTIONS) 

587 LAST_SECTION_A_Q = 13 

588 FIRST_SECTION_B_Q = 14 

589 SECTION_A_QFIELDS = strseq("q", 1, LAST_SECTION_A_Q) 

590 SECTION_B_QFIELDS = strseq("q", FIRST_SECTION_B_Q, NQUESTIONS) 

591 MAX_SCORE = 60 

592 MAX_SECTION_A = 4 * len(SECTION_A_QFIELDS) 

593 MAX_SECTION_B = 4 * len(SECTION_B_QFIELDS) 

594 TASK_FIELDS = QFIELDS + ["period_rated"] 

595 

596 @staticmethod 

597 def longname(req: "CamcopsRequest") -> str: 

598 _ = req.gettext 

599 return _( 

600 "Health of the Nation Outcome Scales, Children and Adolescents" 

601 ) 

602 

603 def is_complete(self) -> bool: 

604 return ( 

605 self.all_fields_not_none(self.TASK_FIELDS) 

606 and self.field_contents_valid() 

607 ) 

608 

609 def section_a_score(self) -> int: 

610 return self._total_score_for_fields(self.SECTION_A_QFIELDS) 

611 

612 def section_b_score(self) -> int: 

613 return self._total_score_for_fields(self.SECTION_B_QFIELDS) 

614 

615 def get_task_html(self, req: CamcopsRequest) -> str: 

616 section_a = "" 

617 for i in range(1, 13 + 1): 

618 section_a += tr_qa( 

619 self.get_q(req, i), 

620 self.get_answer(req, i, getattr(self, "q" + str(i))), 

621 ) 

622 section_b = "" 

623 for i in range(14, self.NQUESTIONS + 1): 

624 section_b += tr_qa( 

625 self.get_q(req, i), 

626 self.get_answer(req, i, getattr(self, "q" + str(i))), 

627 ) 

628 

629 h = """ 

630 <div class="{CssClass.SUMMARY}"> 

631 <table class="{CssClass.SUMMARY}"> 

632 {tr_is_complete} 

633 {total_score} 

634 {section_a_total} 

635 {section_b_total} 

636 </table> 

637 </div> 

638 <table class="{CssClass.TASKDETAIL}"> 

639 <tr> 

640 <th width="50%">Question</th> 

641 <th width="50%">Answer <sup>[1]</sup></th> 

642 </tr> 

643 {period_rated} 

644 {section_a_subhead} 

645 {section_a} 

646 {section_b_subhead} 

647 {section_b} 

648 </table> 

649 <div class="{CssClass.FOOTNOTES}"> 

650 {FOOTNOTE_SCORING} 

651 </div> 

652 {copyright_div} 

653 """.format( 

654 CssClass=CssClass, 

655 tr_is_complete=self.get_is_complete_tr(req), 

656 total_score=tr( 

657 req.sstring(SS.TOTAL_SCORE), 

658 answer(self.total_score()) + f" / {self.MAX_SCORE}", 

659 ), 

660 section_a_total=tr( 

661 self.wxstring(req, "section_a_total"), 

662 answer(self.section_a_score()) + f" / {self.MAX_SECTION_A}", 

663 ), 

664 section_b_total=tr( 

665 self.wxstring(req, "section_b_total"), 

666 answer(self.section_b_score()) + f" / {self.MAX_SECTION_B}", 

667 ), 

668 period_rated=tr_qa( 

669 self.wxstring(req, "period_rated"), self.period_rated 

670 ), 

671 section_a_subhead=subheading_spanning_two_columns( 

672 self.wxstring(req, "section_a_title") 

673 ), 

674 section_a=section_a, 

675 section_b_subhead=subheading_spanning_two_columns( 

676 self.wxstring(req, "section_b_title") 

677 ), 

678 section_b=section_b, 

679 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

680 copyright_div=self.COPYRIGHT_DIV, 

681 ) 

682 return h 

683 

684 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

685 codes = [ 

686 SnomedExpression( 

687 req.snomed(SnomedLookup.HONOSCA_PROCEDURE_ASSESSMENT) 

688 ) 

689 ] 

690 if self.is_complete(): 

691 a = self.section_a_score() 

692 b = self.section_b_score() 

693 total = a + b 

694 codes.append( 

695 SnomedExpression( 

696 req.snomed(SnomedLookup.HONOSCA_SCALE), 

697 { 

698 req.snomed(SnomedLookup.HONOSCA_SCORE): total, 

699 req.snomed(SnomedLookup.HONOSCA_SECTION_A_SCORE): a, 

700 req.snomed(SnomedLookup.HONOSCA_SECTION_B_SCORE): b, 

701 req.snomed( 

702 SnomedLookup.HONOSCA_SECTION_A_PLUS_B_SCORE 

703 ): total, 

704 }, 

705 ) 

706 ) 

707 return codes