Coverage for tasks/icd10specpd.py: 43%
235 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/icd10specpd.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28from typing import Any, List, Optional, Type
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
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)
67# =============================================================================
68# Icd10SpecPD
69# =============================================================================
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))
78class Icd10SpecPD( # type: ignore[misc]
79 TaskHasClinicianMixin,
80 TaskHasPatientMixin,
81 Task,
82):
83 """
84 Server implementation of the ICD10-PD task.
85 """
87 __tablename__ = "icd10specpd"
88 shortname = "ICD10-PD"
89 info_filename_stem = "icd"
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 )
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")
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
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)
338 @staticmethod
339 def longname(req: "CamcopsRequest") -> str:
340 _ = req.gettext
341 return _("ICD-10 criteria for specific personality disorders (F60)")
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
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 ]
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 )
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)
480 def is_complete_paranoid(self) -> bool:
481 return self.all_fields_not_none(self.PARANOID_FIELDS)
483 def is_complete_schizoid(self) -> bool:
484 return self.all_fields_not_none(self.SCHIZOID_FIELDS)
486 def is_complete_dissocial(self) -> bool:
487 return self.all_fields_not_none(self.DISSOCIAL_FIELDS)
489 def is_complete_eu(self) -> bool:
490 return self.all_fields_not_none(self.EU_FIELDS)
492 def is_complete_histrionic(self) -> bool:
493 return self.all_fields_not_none(self.HISTRIONIC_FIELDS)
495 def is_complete_anankastic(self) -> bool:
496 return self.all_fields_not_none(self.ANANKASTIC_FIELDS)
498 def is_complete_anxious(self) -> bool:
499 return self.all_fields_not_none(self.ANXIOUS_FIELDS)
501 def is_complete_dependent(self) -> bool:
502 return self.all_fields_not_none(self.DEPENDENT_FIELDS)
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 )
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
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
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
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
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 )
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
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
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
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
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 )
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 """
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 )
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 """
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 """
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 """
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 )
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
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 )
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 """
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 )
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)
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)
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)
768 # Done
769 h += (
770 """
771 </table>
772 """
773 + ICD10_COPYRIGHT_DIV
774 )
775 return h