Coverage for tasks/honos.py: 52%

181 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/honos.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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

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

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

16 (at your option) any later version. 

17 

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

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

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

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

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

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Optional, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.schema import Column 

35from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import ( 

41 answer, 

42 subheading_spanning_two_columns, 

43 tr, 

44 tr_qa, 

45) 

46from camcops_server.cc_modules.cc_request import CamcopsRequest 

47from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

48from camcops_server.cc_modules.cc_sqla_coltypes import ( 

49 CamcopsColumn, 

50 CharColType, 

51 PermittedValueChecker, 

52) 

53from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

54from camcops_server.cc_modules.cc_task import ( 

55 get_from_dict, 

56 Task, 

57 TaskHasClinicianMixin, 

58 TaskHasPatientMixin, 

59) 

60from camcops_server.cc_modules.cc_text import SS 

61from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

62 

63 

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

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

66 

67FOOTNOTE_SCORING = """ 

68 [1] 0 = no problem; 

69 1 = minor problem requiring no action; 

70 2 = mild problem but definitely present; 

71 3 = moderately severe problem; 

72 4 = severe to very severe problem; 

73 9 = not known. 

74""" 

75 

76 

77# ============================================================================= 

78# HoNOS abstract base class 

79# ============================================================================= 

80 

81# noinspection PyAbstractClass 

82class HonosBase(TaskHasPatientMixin, TaskHasClinicianMixin, Task): 

83 __abstract__ = True 

84 provides_trackers = True 

85 

86 period_rated = Column( 

87 "period_rated", UnicodeText, comment="Period being rated" 

88 ) 

89 

90 COPYRIGHT_DIV = f""" 

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

92 Health of the Nation Outcome Scales: 

93 Copyright © Royal College of Psychiatrists. 

94 Used here with permission. 

95 </div> 

96 """ 

97 

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

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

100 

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

102 return [ 

103 TrackerInfo( 

104 value=self.total_score(), 

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

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

107 axis_min=-0.5, 

108 axis_max=self.MAX_SCORE + 0.5, 

109 ) 

110 ] 

111 

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

113 if not self.is_complete(): 

114 return CTV_INCOMPLETE 

115 return [ 

116 CtvInfo( 

117 content=( 

118 f"{self.shortname} total score " 

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

120 ) 

121 ) 

122 ] 

123 

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

125 return self.standard_task_summary_fields() + [ 

126 SummaryElement( 

127 name="total", 

128 coltype=Integer(), 

129 value=self.total_score(), 

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

131 ) 

132 ] 

133 

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

135 total = 0 

136 for qname in fieldnames: 

137 value = getattr(self, qname) 

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

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

140 total += value 

141 return total 

142 

143 def total_score(self) -> int: 

144 return self._total_score_for_fields(self.QFIELDS) 

145 

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

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

148 

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

150 if a == 9: 

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

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

153 return None 

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

155 

156 

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

158# HoNOS 

159# ============================================================================= 

160 

161 

162class HonosMetaclass(DeclarativeMeta): 

163 # noinspection PyInitNewSignature 

164 def __init__( 

165 cls: Type["Honos"], 

166 name: str, 

167 bases: Tuple[Type, ...], 

168 classdict: Dict[str, Any], 

169 ) -> None: 

170 add_multiple_columns( 

171 cls, 

172 "q", 

173 1, 

174 cls.NQUESTIONS, 

175 pv=PV_MAIN, 

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

177 comment_strings=[ 

178 "overactive/aggressive/disruptive/agitated", 

179 "deliberate self-harm", 

180 "problem-drinking/drug-taking", 

181 "cognitive problems", 

182 "physical illness/disability", 

183 "hallucinations/delusions", 

184 "depressed mood", 

185 "other mental/behavioural problem", 

186 "relationship problems", 

187 "activities of daily living", 

188 "problems with living conditions", 

189 "occupation/activities", 

190 ], 

191 ) 

192 super().__init__(name, bases, classdict) 

193 

194 

195class Honos(HonosBase, metaclass=HonosMetaclass): 

196 """ 

197 Server implementation of the HoNOS task. 

198 """ 

199 

200 __tablename__ = "honos" 

201 shortname = "HoNOS" 

202 info_filename_stem = "honos" 

203 

204 q8problemtype = CamcopsColumn( 

205 "q8problemtype", 

206 CharColType, 

207 permitted_value_checker=PermittedValueChecker( 

208 permitted_values=PV_PROBLEMTYPE 

209 ), 

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

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

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

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

214 ) 

215 q8otherproblem = Column( 

216 "q8otherproblem", UnicodeText, comment="Q8: other problem: specify" 

217 ) 

218 

219 NQUESTIONS = 12 

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

221 MAX_SCORE = 48 

222 

223 @staticmethod 

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

225 _ = req.gettext 

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

227 

228 # noinspection PyUnresolvedReferences 

229 def is_complete(self) -> bool: 

230 if self.any_fields_none(self.QFIELDS): 

231 return False 

232 if not self.field_contents_valid(): 

233 return False 

234 if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: 

235 return False 

236 if ( 

237 self.q8 != 0 

238 and self.q8 != 9 

239 and self.q8problemtype == "J" 

240 and self.q8otherproblem is None 

241 ): 

242 return False 

243 return self.period_rated is not None 

244 

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

246 q8_problem_type_dict = { 

247 None: None, 

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

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

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

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

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

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

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

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

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

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

258 } 

259 one_to_eight = "" 

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

261 one_to_eight += tr_qa( 

262 self.get_q(req, i), 

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

264 ) 

265 nine_onwards = "" 

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

267 nine_onwards += tr_qa( 

268 self.get_q(req, i), 

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

270 ) 

271 

272 h = """ 

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

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

275 {tr_is_complete} 

276 {total_score} 

277 </table> 

278 </div> 

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

280 <tr> 

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

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

283 </tr> 

284 {period_rated} 

285 {one_to_eight} 

286 {q8problemtype} 

287 {q8otherproblem} 

288 {nine_onwards} 

289 </table> 

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

291 {FOOTNOTE_SCORING} 

292 </div> 

293 {copyright_div} 

294 """.format( 

295 CssClass=CssClass, 

296 tr_is_complete=self.get_is_complete_tr(req), 

297 total_score=tr( 

298 req.sstring(SS.TOTAL_SCORE), 

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

300 ), 

301 period_rated=tr_qa( 

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

303 ), 

304 one_to_eight=one_to_eight, 

305 q8problemtype=tr_qa( 

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

307 get_from_dict(q8_problem_type_dict, self.q8problemtype), 

308 ), 

309 q8otherproblem=tr_qa( 

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

311 ), 

312 nine_onwards=nine_onwards, 

313 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

314 copyright_div=self.COPYRIGHT_DIV, 

315 ) 

316 return h 

317 

318 # noinspection PyUnresolvedReferences 

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

320 codes = [ 

321 SnomedExpression( 

322 req.snomed(SnomedLookup.HONOSWA_PROCEDURE_ASSESSMENT) 

323 ) 

324 ] 

325 if self.is_complete(): 

326 codes.append( 

327 SnomedExpression( 

328 req.snomed(SnomedLookup.HONOSWA_SCALE), 

329 { 

330 req.snomed( 

331 SnomedLookup.HONOSWA_SCORE 

332 ): self.total_score(), 

333 req.snomed( 

334 SnomedLookup.HONOSWA_1_OVERACTIVE_SCORE 

335 ): self.q1, # noqa 

336 req.snomed( 

337 SnomedLookup.HONOSWA_2_SELFINJURY_SCORE 

338 ): self.q2, # noqa 

339 req.snomed( 

340 SnomedLookup.HONOSWA_3_SUBSTANCE_SCORE 

341 ): self.q3, # noqa 

342 req.snomed( 

343 SnomedLookup.HONOSWA_4_COGNITIVE_SCORE 

344 ): self.q4, # noqa 

345 req.snomed( 

346 SnomedLookup.HONOSWA_5_PHYSICAL_SCORE 

347 ): self.q5, 

348 req.snomed( 

349 SnomedLookup.HONOSWA_6_PSYCHOSIS_SCORE 

350 ): self.q6, # noqa 

351 req.snomed( 

352 SnomedLookup.HONOSWA_7_DEPRESSION_SCORE 

353 ): self.q7, # noqa 

354 req.snomed( 

355 SnomedLookup.HONOSWA_8_OTHERMENTAL_SCORE 

356 ): self.q8, # noqa 

357 req.snomed( 

358 SnomedLookup.HONOSWA_9_RELATIONSHIPS_SCORE 

359 ): self.q9, # noqa 

360 req.snomed( 

361 SnomedLookup.HONOSWA_10_ADL_SCORE 

362 ): self.q10, 

363 req.snomed( 

364 SnomedLookup.HONOSWA_11_LIVINGCONDITIONS_SCORE 

365 ): self.q11, # noqa 

366 req.snomed( 

367 SnomedLookup.HONOSWA_12_OCCUPATION_SCORE 

368 ): self.q12, # noqa 

369 }, 

370 ) 

371 ) 

372 return codes 

373 

374 

375# ============================================================================= 

376# HoNOS 65+ 

377# ============================================================================= 

378 

379 

380class Honos65Metaclass(DeclarativeMeta): 

381 # noinspection PyInitNewSignature 

382 def __init__( 

383 cls: Type["Honos65"], 

384 name: str, 

385 bases: Tuple[Type, ...], 

386 classdict: Dict[str, Any], 

387 ) -> None: 

388 add_multiple_columns( 

389 cls, 

390 "q", 

391 1, 

392 cls.NQUESTIONS, 

393 pv=PV_MAIN, 

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

395 comment_strings=[ # not exactly identical to HoNOS 

396 "behavioural disturbance", 

397 "deliberate self-harm", 

398 "problem drinking/drug-taking", 

399 "cognitive problems", 

400 "physical illness/disability", 

401 "hallucinations/delusions", 

402 "depressive symptoms", 

403 "other mental/behavioural problem", 

404 "relationship problems", 

405 "activities of daily living", 

406 "living conditions", 

407 "occupation/activities", 

408 ], 

409 ) 

410 super().__init__(name, bases, classdict) 

411 

412 

413class Honos65(HonosBase, metaclass=Honos65Metaclass): 

414 """ 

415 Server implementation of the HoNOS 65+ task. 

416 """ 

417 

418 __tablename__ = "honos65" 

419 shortname = "HoNOS 65+" 

420 info_filename_stem = "honos" 

421 

422 q8problemtype = CamcopsColumn( 

423 "q8problemtype", 

424 CharColType, 

425 permitted_value_checker=PermittedValueChecker( 

426 permitted_values=PV_PROBLEMTYPE 

427 ), 

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

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

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

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

432 ) 

433 q8otherproblem = Column( 

434 "q8otherproblem", UnicodeText, comment="Q8: other problem: specify" 

435 ) 

436 

437 NQUESTIONS = 12 

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

439 MAX_SCORE = 48 

440 

441 @staticmethod 

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

443 _ = req.gettext 

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

445 

446 # noinspection PyUnresolvedReferences 

447 def is_complete(self) -> bool: 

448 if self.any_fields_none(self.QFIELDS): 

449 return False 

450 if not self.field_contents_valid(): 

451 return False 

452 if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: 

453 return False 

454 if ( 

455 self.q8 != 0 

456 and self.q8 != 9 

457 and self.q8problemtype == "J" 

458 and self.q8otherproblem is None 

459 ): 

460 return False 

461 return self.period_rated is not None 

462 

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

464 q8_problem_type_dict = { 

465 None: None, 

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

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

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

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

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

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

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

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

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

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

476 } 

477 one_to_eight = "" 

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

479 one_to_eight += tr_qa( 

480 self.get_q(req, i), 

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

482 ) 

483 nine_onwards = "" 

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

485 nine_onwards += tr_qa( 

486 self.get_q(req, i), 

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

488 ) 

489 

490 h = """ 

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

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

493 {tr_is_complete} 

494 {total_score} 

495 </table> 

496 </div> 

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

498 <tr> 

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

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

501 </tr> 

502 {period_rated} 

503 {one_to_eight} 

504 {q8problemtype} 

505 {q8otherproblem} 

506 {nine_onwards} 

507 </table> 

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

509 {FOOTNOTE_SCORING} 

510 </div> 

511 {copyright_div} 

512 """.format( 

513 CssClass=CssClass, 

514 tr_is_complete=self.get_is_complete_tr(req), 

515 total_score=tr( 

516 req.sstring(SS.TOTAL_SCORE), 

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

518 ), 

519 period_rated=tr_qa( 

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

521 ), 

522 one_to_eight=one_to_eight, 

523 q8problemtype=tr_qa( 

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

525 get_from_dict(q8_problem_type_dict, self.q8problemtype), 

526 ), 

527 q8otherproblem=tr_qa( 

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

529 ), 

530 nine_onwards=nine_onwards, 

531 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

532 copyright_div=self.COPYRIGHT_DIV, 

533 ) 

534 return h 

535 

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

537 codes = [ 

538 SnomedExpression( 

539 req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT) 

540 ) 

541 ] 

542 if self.is_complete(): 

543 codes.append( 

544 SnomedExpression( 

545 req.snomed(SnomedLookup.HONOS65_SCALE), 

546 { 

547 req.snomed( 

548 SnomedLookup.HONOS65_SCORE 

549 ): self.total_score() 

550 }, 

551 ) 

552 ) 

553 return codes 

554 

555 

556# ============================================================================= 

557# HoNOSCA 

558# ============================================================================= 

559 

560 

561class HonoscaMetaclass(DeclarativeMeta): 

562 # noinspection PyInitNewSignature 

563 def __init__( 

564 cls: Type["Honosca"], 

565 name: str, 

566 bases: Tuple[Type, ...], 

567 classdict: Dict[str, Any], 

568 ) -> None: 

569 add_multiple_columns( 

570 cls, 

571 "q", 

572 1, 

573 cls.NQUESTIONS, 

574 pv=PV_MAIN, 

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

576 comment_strings=[ 

577 "disruptive/antisocial/aggressive", 

578 "overactive/inattentive", 

579 "self-harm", 

580 "alcohol/drug misuse", 

581 "scholastic/language problems", 

582 "physical illness/disability", 

583 "delusions/hallucinations", 

584 "non-organic somatic symptoms", 

585 "emotional symptoms", 

586 "peer relationships", 

587 "self-care and independence", 

588 "family life/relationships", 

589 "school attendance", 

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

591 "lack of information about services", 

592 ], 

593 ) 

594 super().__init__(name, bases, classdict) 

595 

596 

597class Honosca(HonosBase, metaclass=HonoscaMetaclass): 

598 """ 

599 Server implementation of the HoNOSCA task. 

600 """ 

601 

602 __tablename__ = "honosca" 

603 shortname = "HoNOSCA" 

604 info_filename_stem = "honos" 

605 

606 NQUESTIONS = 15 

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

608 LAST_SECTION_A_Q = 13 

609 FIRST_SECTION_B_Q = 14 

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

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

612 MAX_SCORE = 60 

613 MAX_SECTION_A = 4 * len(SECTION_A_QFIELDS) 

614 MAX_SECTION_B = 4 * len(SECTION_B_QFIELDS) 

615 TASK_FIELDS = QFIELDS + ["period_rated"] 

616 

617 @staticmethod 

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

619 _ = req.gettext 

620 return _( 

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

622 ) 

623 

624 def is_complete(self) -> bool: 

625 return ( 

626 self.all_fields_not_none(self.TASK_FIELDS) 

627 and self.field_contents_valid() 

628 ) 

629 

630 def section_a_score(self) -> int: 

631 return self._total_score_for_fields(self.SECTION_A_QFIELDS) 

632 

633 def section_b_score(self) -> int: 

634 return self._total_score_for_fields(self.SECTION_B_QFIELDS) 

635 

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

637 section_a = "" 

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

639 section_a += tr_qa( 

640 self.get_q(req, i), 

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

642 ) 

643 section_b = "" 

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

645 section_b += tr_qa( 

646 self.get_q(req, i), 

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

648 ) 

649 

650 h = """ 

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

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

653 {tr_is_complete} 

654 {total_score} 

655 {section_a_total} 

656 {section_b_total} 

657 </table> 

658 </div> 

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

660 <tr> 

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

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

663 </tr> 

664 {period_rated} 

665 {section_a_subhead} 

666 {section_a} 

667 {section_b_subhead} 

668 {section_b} 

669 </table> 

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

671 {FOOTNOTE_SCORING} 

672 </div> 

673 {copyright_div} 

674 """.format( 

675 CssClass=CssClass, 

676 tr_is_complete=self.get_is_complete_tr(req), 

677 total_score=tr( 

678 req.sstring(SS.TOTAL_SCORE), 

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

680 ), 

681 section_a_total=tr( 

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

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

684 ), 

685 section_b_total=tr( 

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

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

688 ), 

689 period_rated=tr_qa( 

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

691 ), 

692 section_a_subhead=subheading_spanning_two_columns( 

693 self.wxstring(req, "section_a_title") 

694 ), 

695 section_a=section_a, 

696 section_b_subhead=subheading_spanning_two_columns( 

697 self.wxstring(req, "section_b_title") 

698 ), 

699 section_b=section_b, 

700 FOOTNOTE_SCORING=FOOTNOTE_SCORING, 

701 copyright_div=self.COPYRIGHT_DIV, 

702 ) 

703 return h 

704 

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

706 codes = [ 

707 SnomedExpression( 

708 req.snomed(SnomedLookup.HONOSCA_PROCEDURE_ASSESSMENT) 

709 ) 

710 ] 

711 if self.is_complete(): 

712 a = self.section_a_score() 

713 b = self.section_b_score() 

714 total = a + b 

715 codes.append( 

716 SnomedExpression( 

717 req.snomed(SnomedLookup.HONOSCA_SCALE), 

718 { 

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

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

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

722 req.snomed( 

723 SnomedLookup.HONOSCA_SECTION_A_PLUS_B_SCORE 

724 ): total, # noqa 

725 }, 

726 ) 

727 ) 

728 return codes