Coverage for tasks/icd10specpd.py: 43%

235 statements  

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

1""" 

2camcops_server/tasks/icd10specpd.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.datetimefunc import format_datetime 

31import cardinal_pythonlib.rnc_web as ws 

32from cardinal_pythonlib.stringfunc import strseq 

33from cardinal_pythonlib.typetests import is_false 

34from sqlalchemy.orm import Mapped 

35from sqlalchemy.sql.schema import Column 

36from sqlalchemy.sql.sqltypes import Boolean, Date, UnicodeText 

37 

38from camcops_server.cc_modules.cc_constants import ( 

39 CssClass, 

40 DateFormat, 

41 ICD10_COPYRIGHT_DIV, 

42 PV, 

43) 

44from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

45from camcops_server.cc_modules.cc_db import add_multiple_columns 

46from camcops_server.cc_modules.cc_html import ( 

47 answer, 

48 get_yes_no_none, 

49 get_yes_no_unknown, 

50 subheading_spanning_two_columns, 

51 tr_qa, 

52) 

53from camcops_server.cc_modules.cc_request import CamcopsRequest 

54from camcops_server.cc_modules.cc_sqla_coltypes import ( 

55 BIT_CHECKER, 

56 mapped_camcops_column, 

57) 

58from camcops_server.cc_modules.cc_string import AS 

59from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

60from camcops_server.cc_modules.cc_task import ( 

61 Task, 

62 TaskHasClinicianMixin, 

63 TaskHasPatientMixin, 

64) 

65 

66 

67# ============================================================================= 

68# Icd10SpecPD 

69# ============================================================================= 

70 

71 

72def ctv_info_pd( 

73 req: CamcopsRequest, condition: str, has_it: Optional[bool] 

74) -> CtvInfo: 

75 return CtvInfo(content=condition + ": " + get_yes_no_unknown(req, has_it)) 

76 

77 

78class Icd10SpecPD( # type: ignore[misc] 

79 TaskHasClinicianMixin, 

80 TaskHasPatientMixin, 

81 Task, 

82): 

83 """ 

84 Server implementation of the ICD10-PD task. 

85 """ 

86 

87 __tablename__ = "icd10specpd" 

88 shortname = "ICD10-PD" 

89 info_filename_stem = "icd" 

90 

91 @classmethod 

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

93 add_multiple_columns( 

94 cls, 

95 "g", 

96 1, 

97 cls.N_GENERAL, 

98 Boolean, 

99 pv=PV.BIT, 

100 comment_fmt="G{n}: {s}", 

101 comment_strings=[ 

102 "pathological 1", 

103 "pervasive", 

104 "pathological 2", 

105 "persistent", 

106 "primary 1", 

107 "primary 2", 

108 ], 

109 ) 

110 add_multiple_columns( 

111 cls, 

112 "g1_", 

113 1, 

114 cls.N_GENERAL_1, 

115 Boolean, 

116 pv=PV.BIT, 

117 comment_fmt="G1{n}: {s}", 

118 comment_strings=[ 

119 "cognition", 

120 "affectivity", 

121 "impulse control", 

122 "interpersonal", 

123 ], 

124 ) 

125 add_multiple_columns( 

126 cls, 

127 "paranoid", 

128 1, 

129 cls.N_PARANOID, 

130 Boolean, 

131 pv=PV.BIT, 

132 comment_fmt="Paranoid ({n}): {s}", 

133 comment_strings=[ 

134 "sensitive", 

135 "grudges", 

136 "suspicious", 

137 "personal rights", 

138 "sexual jealousy", 

139 "self-referential", 

140 "conspiratorial", 

141 ], 

142 ) 

143 add_multiple_columns( 

144 cls, 

145 "schizoid", 

146 1, 

147 cls.N_SCHIZOID, 

148 Boolean, 

149 pv=PV.BIT, 

150 comment_fmt="Schizoid ({n}): {s}", 

151 comment_strings=[ 

152 "little pleasure", 

153 "cold/detached", 

154 "limited capacity for warmth", 

155 "indifferent to praise/criticism", 

156 "little interest in sex", 

157 "solitary", 

158 "fantasy/introspection", 

159 "0/1 close friends/confidants", 

160 "insensitive to social norms", 

161 ], 

162 ) 

163 add_multiple_columns( 

164 cls, 

165 "dissocial", 

166 1, 

167 cls.N_DISSOCIAL, 

168 Boolean, 

169 pv=PV.BIT, 

170 comment_fmt="Dissocial ({n}): {s}", 

171 comment_strings=[ 

172 "unconcern", 

173 "irresponsibility", 

174 "incapacity to maintain relationships", 

175 "low tolerance to frustration", 

176 "incapacity for guilt", 

177 "prone to blame others", 

178 ], 

179 ) 

180 add_multiple_columns( 

181 cls, 

182 "eu", 

183 1, 

184 cls.N_EU, 

185 Boolean, 

186 pv=PV.BIT, 

187 comment_fmt="Emotionally unstable ({n}): {s}", 

188 comment_strings=[ 

189 "act without considering consequences", 

190 "quarrelsome", 

191 "outbursts of anger", 

192 "can't maintain actions with immediate reward", 

193 "unstable/capricious mood", 

194 "uncertain self-image", 

195 "intense/unstable relationships", 

196 "avoids abandonment", 

197 "threats/acts of self-harm", 

198 "feelings of emptiness", 

199 ], 

200 ) 

201 add_multiple_columns( 

202 cls, 

203 "histrionic", 

204 1, 

205 cls.N_HISTRIONIC, 

206 Boolean, 

207 pv=PV.BIT, 

208 comment_fmt="Histrionic ({n}): {s}", 

209 comment_strings=[ 

210 "theatricality", 

211 "suggestibility", 

212 "shallow/labile affect", 

213 "centre of attention", 

214 "inappropriately seductive", 

215 "concerned with attractiveness", 

216 ], 

217 ) 

218 add_multiple_columns( 

219 cls, 

220 "anankastic", 

221 1, 

222 cls.N_ANANKASTIC, 

223 Boolean, 

224 pv=PV.BIT, 

225 comment_fmt="Anankastic ({n}): {s}", 

226 comment_strings=[ 

227 "doubt/caution", 

228 "preoccupation with details", 

229 "perfectionism", 

230 "excessively conscientious", 

231 "preoccupied with productivity", 

232 "excessive pedantry", 

233 "rigid/stubborn", 

234 "require others do things specific way", 

235 ], 

236 ) 

237 add_multiple_columns( 

238 cls, 

239 "anxious", 

240 1, 

241 cls.N_ANXIOUS, 

242 Boolean, 

243 pv=PV.BIT, 

244 comment_fmt="Anxious ({n}), {s}", 

245 comment_strings=[ 

246 "tension/apprehension", 

247 "preoccupied with criticism/rejection", 

248 "won't get involved unless certain liked", 

249 "need for security restricts lifestyle", 

250 "avoidance of interpersonal contact", 

251 ], 

252 ) 

253 add_multiple_columns( 

254 cls, 

255 "dependent", 

256 1, 

257 cls.N_DEPENDENT, 

258 Boolean, 

259 pv=PV.BIT, 

260 comment_fmt="Dependent ({n}): {s}", 

261 comment_strings=[ 

262 "others decide", 

263 "subordinate needs to those of others", 

264 "unwilling to make reasonable demands", 

265 "uncomfortable/helpless when alone", 

266 "fears of being left to oneself", 

267 "everyday decisions require advice/reassurance", 

268 ], 

269 ) 

270 

271 date_pertains_to = Column( 

272 "date_pertains_to", Date, comment="Date the assessment pertains to" 

273 ) 

274 comments = Column("comments", UnicodeText, comment="Clinician's comments") 

275 skip_paranoid: Mapped[Optional[bool]] = mapped_camcops_column( 

276 permitted_value_checker=BIT_CHECKER, 

277 comment="Skip questions for paranoid PD?", 

278 ) 

279 skip_schizoid: Mapped[Optional[bool]] = mapped_camcops_column( 

280 permitted_value_checker=BIT_CHECKER, 

281 comment="Skip questions for schizoid PD?", 

282 ) 

283 skip_dissocial: Mapped[Optional[bool]] = mapped_camcops_column( 

284 permitted_value_checker=BIT_CHECKER, 

285 comment="Skip questions for dissocial PD?", 

286 ) 

287 skip_eu: Mapped[Optional[bool]] = mapped_camcops_column( 

288 permitted_value_checker=BIT_CHECKER, 

289 comment="Skip questions for emotionally unstable PD?", 

290 ) 

291 skip_histrionic: Mapped[Optional[bool]] = mapped_camcops_column( 

292 permitted_value_checker=BIT_CHECKER, 

293 comment="Skip questions for histrionic PD?", 

294 ) 

295 skip_anankastic: Mapped[Optional[bool]] = mapped_camcops_column( 

296 permitted_value_checker=BIT_CHECKER, 

297 comment="Skip questions for anankastic PD?", 

298 ) 

299 skip_anxious: Mapped[Optional[bool]] = mapped_camcops_column( 

300 permitted_value_checker=BIT_CHECKER, 

301 comment="Skip questions for anxious PD?", 

302 ) 

303 skip_dependent: Mapped[Optional[bool]] = mapped_camcops_column( 

304 permitted_value_checker=BIT_CHECKER, 

305 comment="Skip questions for dependent PD?", 

306 ) 

307 other_pd_present: Mapped[Optional[bool]] = mapped_camcops_column( 

308 permitted_value_checker=BIT_CHECKER, 

309 comment="Is another personality disorder present?", 

310 ) 

311 vignette = Column("vignette", UnicodeText, comment="Vignette") 

312 

313 N_GENERAL = 6 

314 N_GENERAL_1 = 4 

315 N_PARANOID = 7 

316 N_SCHIZOID = 9 

317 N_DISSOCIAL = 6 

318 N_EU = 10 

319 N_EUPD_I = 5 

320 N_HISTRIONIC = 6 

321 N_ANANKASTIC = 8 

322 N_ANXIOUS = 5 

323 N_DEPENDENT = 6 

324 

325 GENERAL_FIELDS = strseq("g", 1, N_GENERAL) 

326 GENERAL_1_FIELDS = strseq("g1_", 1, N_GENERAL_1) 

327 PARANOID_FIELDS = strseq("paranoid", 1, N_PARANOID) 

328 SCHIZOID_FIELDS = strseq("schizoid", 1, N_SCHIZOID) 

329 DISSOCIAL_FIELDS = strseq("dissocial", 1, N_DISSOCIAL) 

330 EU_FIELDS = strseq("eu", 1, N_EU) 

331 EUPD_I_FIELDS = strseq("eu", 1, N_EUPD_I) # impulsive 

332 EUPD_B_FIELDS = strseq("eu", N_EUPD_I + 1, N_EU) # borderline 

333 HISTRIONIC_FIELDS = strseq("histrionic", 1, N_HISTRIONIC) 

334 ANANKASTIC_FIELDS = strseq("anankastic", 1, N_ANANKASTIC) 

335 ANXIOUS_FIELDS = strseq("anxious", 1, N_ANXIOUS) 

336 DEPENDENT_FIELDS = strseq("dependent", 1, N_DEPENDENT) 

337 

338 @staticmethod 

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

340 _ = req.gettext 

341 return _("ICD-10 criteria for specific personality disorders (F60)") 

342 

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

344 if not self.is_complete(): 

345 return CTV_INCOMPLETE 

346 infolist = [ 

347 ctv_info_pd( 

348 req, 

349 self.wxstring(req, "meets_general_criteria"), 

350 self.has_pd(), 

351 ), 

352 ctv_info_pd( 

353 req, 

354 self.wxstring(req, "paranoid_pd_title"), 

355 self.has_paranoid_pd(), 

356 ), 

357 ctv_info_pd( 

358 req, 

359 self.wxstring(req, "schizoid_pd_title"), 

360 self.has_schizoid_pd(), 

361 ), 

362 ctv_info_pd( 

363 req, 

364 self.wxstring(req, "dissocial_pd_title"), 

365 self.has_dissocial_pd(), 

366 ), 

367 ctv_info_pd( 

368 req, self.wxstring(req, "eu_pd_i_title"), self.has_eupd_i() 

369 ), 

370 ctv_info_pd( 

371 req, self.wxstring(req, "eu_pd_b_title"), self.has_eupd_b() 

372 ), 

373 ctv_info_pd( 

374 req, 

375 self.wxstring(req, "histrionic_pd_title"), 

376 self.has_histrionic_pd(), 

377 ), 

378 ctv_info_pd( 

379 req, 

380 self.wxstring(req, "anankastic_pd_title"), 

381 self.has_anankastic_pd(), 

382 ), 

383 ctv_info_pd( 

384 req, 

385 self.wxstring(req, "anxious_pd_title"), 

386 self.has_anxious_pd(), 

387 ), 

388 ctv_info_pd( 

389 req, 

390 self.wxstring(req, "dependent_pd_title"), 

391 self.has_dependent_pd(), 

392 ), 

393 ] 

394 return infolist 

395 

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

397 return self.standard_task_summary_fields() + [ 

398 SummaryElement( 

399 name="meets_general_criteria", 

400 coltype=Boolean(), 

401 value=self.has_pd(), 

402 comment="Meets general criteria for personality disorder?", 

403 ), 

404 SummaryElement( 

405 name="paranoid_pd", 

406 coltype=Boolean(), 

407 value=self.has_paranoid_pd(), 

408 comment="Meets criteria for paranoid PD?", 

409 ), 

410 SummaryElement( 

411 name="schizoid_pd", 

412 coltype=Boolean(), 

413 value=self.has_schizoid_pd(), 

414 comment="Meets criteria for schizoid PD?", 

415 ), 

416 SummaryElement( 

417 name="dissocial_pd", 

418 coltype=Boolean(), 

419 value=self.has_dissocial_pd(), 

420 comment="Meets criteria for dissocial PD?", 

421 ), 

422 SummaryElement( 

423 name="eupd_i", 

424 coltype=Boolean(), 

425 value=self.has_eupd_i(), 

426 comment="Meets criteria for EUPD (impulsive type)?", 

427 ), 

428 SummaryElement( 

429 name="eupd_b", 

430 coltype=Boolean(), 

431 value=self.has_eupd_b(), 

432 comment="Meets criteria for EUPD (borderline type)?", 

433 ), 

434 SummaryElement( 

435 name="histrionic_pd", 

436 coltype=Boolean(), 

437 value=self.has_histrionic_pd(), 

438 comment="Meets criteria for histrionic PD?", 

439 ), 

440 SummaryElement( 

441 name="anankastic_pd", 

442 coltype=Boolean(), 

443 value=self.has_anankastic_pd(), 

444 comment="Meets criteria for anankastic PD?", 

445 ), 

446 SummaryElement( 

447 name="anxious_pd", 

448 coltype=Boolean(), 

449 value=self.has_anxious_pd(), 

450 comment="Meets criteria for anxious PD?", 

451 ), 

452 SummaryElement( 

453 name="dependent_pd", 

454 coltype=Boolean(), 

455 value=self.has_dependent_pd(), 

456 comment="Meets criteria for dependent PD?", 

457 ), 

458 ] 

459 

460 # noinspection PyUnresolvedReferences 

461 def is_pd_excluded(self) -> bool: 

462 return ( 

463 is_false(self.g1) # type: ignore[attr-defined] 

464 or is_false(self.g2) # type: ignore[attr-defined] 

465 or is_false(self.g3) # type: ignore[attr-defined] 

466 or is_false(self.g4) # type: ignore[attr-defined] 

467 or is_false(self.g5) # type: ignore[attr-defined] 

468 or is_false(self.g6) # type: ignore[attr-defined] 

469 or ( 

470 self.all_fields_not_none(self.GENERAL_1_FIELDS) 

471 and self.count_booleans(self.GENERAL_1_FIELDS) <= 1 

472 ) 

473 ) 

474 

475 def is_complete_general(self) -> bool: 

476 return self.all_fields_not_none( 

477 self.GENERAL_FIELDS 

478 ) and self.all_fields_not_none(self.GENERAL_1_FIELDS) 

479 

480 def is_complete_paranoid(self) -> bool: 

481 return self.all_fields_not_none(self.PARANOID_FIELDS) 

482 

483 def is_complete_schizoid(self) -> bool: 

484 return self.all_fields_not_none(self.SCHIZOID_FIELDS) 

485 

486 def is_complete_dissocial(self) -> bool: 

487 return self.all_fields_not_none(self.DISSOCIAL_FIELDS) 

488 

489 def is_complete_eu(self) -> bool: 

490 return self.all_fields_not_none(self.EU_FIELDS) 

491 

492 def is_complete_histrionic(self) -> bool: 

493 return self.all_fields_not_none(self.HISTRIONIC_FIELDS) 

494 

495 def is_complete_anankastic(self) -> bool: 

496 return self.all_fields_not_none(self.ANANKASTIC_FIELDS) 

497 

498 def is_complete_anxious(self) -> bool: 

499 return self.all_fields_not_none(self.ANXIOUS_FIELDS) 

500 

501 def is_complete_dependent(self) -> bool: 

502 return self.all_fields_not_none(self.DEPENDENT_FIELDS) 

503 

504 # Meets criteria? These also return null for unknown. 

505 def has_pd(self) -> Optional[bool]: 

506 if self.is_pd_excluded(): 

507 return False 

508 if not self.is_complete_general(): 

509 return None 

510 return ( 

511 self.all_truthy(self.GENERAL_FIELDS) 

512 and self.count_booleans(self.GENERAL_1_FIELDS) > 1 

513 ) 

514 

515 def has_paranoid_pd(self) -> Optional[bool]: 

516 hpd = self.has_pd() 

517 if not hpd: 

518 return hpd 

519 if not self.is_complete_paranoid(): 

520 return None 

521 return self.count_booleans(self.PARANOID_FIELDS) >= 4 

522 

523 def has_schizoid_pd(self) -> Optional[bool]: 

524 hpd = self.has_pd() 

525 if not hpd: 

526 return hpd 

527 if not self.is_complete_schizoid(): 

528 return None 

529 return self.count_booleans(self.SCHIZOID_FIELDS) >= 4 

530 

531 def has_dissocial_pd(self) -> Optional[bool]: 

532 hpd = self.has_pd() 

533 if not hpd: 

534 return hpd 

535 if not self.is_complete_dissocial(): 

536 return None 

537 return self.count_booleans(self.DISSOCIAL_FIELDS) >= 3 

538 

539 # noinspection PyUnresolvedReferences 

540 def has_eupd_i(self) -> Optional[bool]: 

541 hpd = self.has_pd() 

542 if not hpd: 

543 return hpd 

544 if not self.is_complete_eu(): 

545 return None 

546 return self.count_booleans(self.EUPD_I_FIELDS) >= 3 and self.eu2 # type: ignore[attr-defined] # noqa: E501 

547 

548 def has_eupd_b(self) -> Optional[bool]: 

549 hpd = self.has_pd() 

550 if not hpd: 

551 return hpd 

552 if not self.is_complete_eu(): 

553 return None 

554 return ( 

555 self.count_booleans(self.EUPD_I_FIELDS) >= 3 

556 and self.count_booleans(self.EUPD_B_FIELDS) >= 2 

557 ) 

558 

559 def has_histrionic_pd(self) -> Optional[bool]: 

560 hpd = self.has_pd() 

561 if not hpd: 

562 return hpd 

563 if not self.is_complete_histrionic(): 

564 return None 

565 return self.count_booleans(self.HISTRIONIC_FIELDS) >= 4 

566 

567 def has_anankastic_pd(self) -> Optional[bool]: 

568 hpd = self.has_pd() 

569 if not hpd: 

570 return hpd 

571 if not self.is_complete_anankastic(): 

572 return None 

573 return self.count_booleans(self.ANANKASTIC_FIELDS) >= 4 

574 

575 def has_anxious_pd(self) -> Optional[bool]: 

576 hpd = self.has_pd() 

577 if not hpd: 

578 return hpd 

579 if not self.is_complete_anxious(): 

580 return None 

581 return self.count_booleans(self.ANXIOUS_FIELDS) >= 4 

582 

583 def has_dependent_pd(self) -> Optional[bool]: 

584 hpd = self.has_pd() 

585 if not hpd: 

586 return hpd 

587 if not self.is_complete_dependent(): 

588 return None 

589 return self.count_booleans(self.DEPENDENT_FIELDS) >= 4 

590 

591 def is_complete(self) -> bool: 

592 return ( 

593 self.date_pertains_to is not None 

594 and ( 

595 self.is_pd_excluded() 

596 or ( 

597 self.is_complete_general() 

598 and (self.skip_paranoid or self.is_complete_paranoid()) 

599 and (self.skip_schizoid or self.is_complete_schizoid()) 

600 and (self.skip_dissocial or self.is_complete_dissocial()) 

601 and (self.skip_eu or self.is_complete_eu()) 

602 and (self.skip_histrionic or self.is_complete_histrionic()) 

603 and (self.skip_anankastic or self.is_complete_anankastic()) 

604 and (self.skip_anxious or self.is_complete_anxious()) 

605 and (self.skip_dependent or self.is_complete_dependent()) 

606 ) 

607 ) 

608 and self.field_contents_valid() 

609 ) 

610 

611 def pd_heading(self, req: CamcopsRequest, wstringname: str) -> str: 

612 return f""" 

613 <tr class="{CssClass.HEADING}"> 

614 <td colspan="2">{self.wxstring(req, wstringname)}</td> 

615 </tr> 

616 """ 

617 

618 def pd_skiprow(self, req: CamcopsRequest, stem: str) -> str: 

619 return self.get_twocol_bool_row( 

620 req, "skip_" + stem, label=self.wxstring(req, "skip_this_pd") 

621 ) 

622 

623 def pd_subheading(self, req: CamcopsRequest, wstringname: str) -> str: 

624 return f""" 

625 <tr class="{CssClass.SUBHEADING}"> 

626 <td colspan="2">{self.wxstring(req, wstringname)}</td> 

627 </tr> 

628 """ 

629 

630 def pd_general_criteria_bits(self, req: CamcopsRequest) -> str: 

631 return f""" 

632 <tr> 

633 <td>{self.wxstring(req, "general_criteria_must_be_met")}</td> 

634 <td><i><b>{get_yes_no_unknown(req, self.has_pd())}</b></i></td> 

635 </tr> 

636 """ 

637 

638 def pd_b_text(self, req: CamcopsRequest, wstringname: str) -> str: 

639 return f""" 

640 <tr> 

641 <td>{self.wxstring(req, wstringname)}</td> 

642 <td class="{CssClass.SUBHEADING}"></td> 

643 </tr> 

644 """ 

645 

646 def pd_basic_row(self, req: CamcopsRequest, stem: str, i: int) -> str: 

647 return self.get_twocol_bool_row_true_false( 

648 req, stem + str(i), self.wxstring(req, stem + str(i)) 

649 ) 

650 

651 def standard_pd_html(self, req: CamcopsRequest, stem: str, n: int) -> str: 

652 html = self.pd_heading(req, stem + "_pd_title") 

653 html += self.pd_skiprow(req, stem) 

654 html += self.pd_general_criteria_bits(req) 

655 html += self.pd_b_text(req, stem + "_pd_B") 

656 for i in range(1, n + 1): 

657 html += self.pd_basic_row(req, stem, i) 

658 return html 

659 

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

661 h = self.get_standard_clinician_comments_block(req, self.comments) # type: ignore[arg-type] # noqa: E501 

662 h += f""" 

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

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

665 """ 

666 h += self.get_is_complete_tr(req) 

667 h += tr_qa( 

668 req.wappstring(AS.DATE_PERTAINS_TO), 

669 format_datetime( 

670 self.date_pertains_to, DateFormat.LONG_DATE, default=None # type: ignore[arg-type] # noqa: E501 

671 ), 

672 ) 

673 h += tr_qa( 

674 self.wxstring(req, "meets_general_criteria"), 

675 get_yes_no_none(req, self.has_pd()), 

676 ) 

677 h += tr_qa( 

678 self.wxstring(req, "paranoid_pd_title"), 

679 get_yes_no_none(req, self.has_paranoid_pd()), 

680 ) 

681 h += tr_qa( 

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

683 get_yes_no_none(req, self.has_schizoid_pd()), 

684 ) 

685 h += tr_qa( 

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

687 get_yes_no_none(req, self.has_dissocial_pd()), 

688 ) 

689 h += tr_qa( 

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

691 get_yes_no_none(req, self.has_eupd_i()), 

692 ) 

693 h += tr_qa( 

694 self.wxstring(req, "eu_pd_b_title"), 

695 get_yes_no_none(req, self.has_eupd_b()), 

696 ) 

697 h += tr_qa( 

698 self.wxstring(req, "histrionic_pd_title"), 

699 get_yes_no_none(req, self.has_histrionic_pd()), 

700 ) 

701 h += tr_qa( 

702 self.wxstring(req, "anankastic_pd_title"), 

703 get_yes_no_none(req, self.has_anankastic_pd()), 

704 ) 

705 h += tr_qa( 

706 self.wxstring(req, "anxious_pd_title"), 

707 get_yes_no_none(req, self.has_anxious_pd()), 

708 ) 

709 h += tr_qa( 

710 self.wxstring(req, "dependent_pd_title"), 

711 get_yes_no_none(req, self.has_dependent_pd()), 

712 ) 

713 

714 h += f""" 

715 </table> 

716 </div> 

717 <div> 

718 <p><i>Vignette:</i></p> 

719 <p>{answer(ws.webify(self.vignette), 

720 default_for_blank_strings=True)}</p> 

721 </div> 

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

723 <tr> 

724 <th width="80%">Question</th> 

725 <th width="20%">Answer</th> 

726 </tr> 

727 """ 

728 

729 # General 

730 h += subheading_spanning_two_columns(self.wxstring(req, "general")) 

731 h += self.get_twocol_bool_row_true_false( 

732 req, "g1", self.wxstring(req, "G1") 

733 ) 

734 h += self.pd_b_text(req, "G1b") 

735 for i in range(1, Icd10SpecPD.N_GENERAL_1 + 1): 

736 h += self.get_twocol_bool_row_true_false( 

737 req, "g1_" + str(i), self.wxstring(req, "G1_" + str(i)) 

738 ) 

739 for i in range(2, Icd10SpecPD.N_GENERAL + 1): 

740 h += self.get_twocol_bool_row_true_false( 

741 req, "g" + str(i), self.wxstring(req, "G" + str(i)) 

742 ) 

743 

744 # Paranoid, etc. 

745 h += self.standard_pd_html(req, "paranoid", Icd10SpecPD.N_PARANOID) 

746 h += self.standard_pd_html(req, "schizoid", Icd10SpecPD.N_SCHIZOID) 

747 h += self.standard_pd_html(req, "dissocial", Icd10SpecPD.N_DISSOCIAL) 

748 

749 # EUPD is special 

750 h += self.pd_heading(req, "eu_pd_title") 

751 h += self.pd_skiprow(req, "eu") 

752 h += self.pd_general_criteria_bits(req) 

753 h += self.pd_subheading(req, "eu_pd_i_title") 

754 h += self.pd_b_text(req, "eu_pd_i_B") 

755 for i in range(1, Icd10SpecPD.N_EUPD_I + 1): 

756 h += self.pd_basic_row(req, "eu", i) 

757 h += self.pd_subheading(req, "eu_pd_b_title") 

758 h += self.pd_b_text(req, "eu_pd_b_B") 

759 for i in range(Icd10SpecPD.N_EUPD_I + 1, Icd10SpecPD.N_EU + 1): 

760 h += self.pd_basic_row(req, "eu", i) 

761 

762 # Back to plain ones 

763 h += self.standard_pd_html(req, "histrionic", Icd10SpecPD.N_HISTRIONIC) 

764 h += self.standard_pd_html(req, "anankastic", Icd10SpecPD.N_ANANKASTIC) 

765 h += self.standard_pd_html(req, "anxious", Icd10SpecPD.N_ANXIOUS) 

766 h += self.standard_pd_html(req, "dependent", Icd10SpecPD.N_DEPENDENT) 

767 

768 # Done 

769 h += ( 

770 """ 

771 </table> 

772 """ 

773 + ICD10_COPYRIGHT_DIV 

774 ) 

775 return h