Coverage for cc_modules/cc_html.py: 35%

1"""

2 camcops_server/cc_modules/cc_html.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**Basic HTML creation functions.**

27

28"""

29

30 import base64

31 from typing import Any, Callable, List, Optional, TYPE_CHECKING, Union

32

33 import cardinal_pythonlib.rnc_web as ws

34

35 from camcops_server.cc_modules.cc_constants import CssClass

36 from camcops_server.cc_modules.cc_text import SS

37

38 if TYPE_CHECKING:

39 from camcops_server.cc_modules.cc_request import CamcopsRequest

40

41

42# =============================================================================

43# HTML elements

44# =============================================================================

45

46

47 def table_row(

48 columns: List[str],

49 classes: List[str] = None,

50 colspans: List[Union[str, int]] = None,

51 colwidths: List[str] = None,

52 default: str = "",

53 heading: bool = False,

54) -> str:

55 """

56 Make HTML table row.

57

58 Args:

59 columns: contents of HTML table columns

60 classes: optional CSS classes, one for each column

61 colspans: ``colspan`` values for each column

62 colwidths: ``width`` values for each column

63 default: content to use if a ``column`` value is None

64 heading: use ``<th>`` rather than ``<td>`` for contents?

65

66 Returns:

67 the ``<tr>...</tr>`` string

68 """

69 n = len(columns)

70

71 if not classes or len(classes) != n:

72 # blank, or duff (in which case ignore)

73 classes = [""] * n

74 else:

75 classes = [(f' class="{x}"' if x else "") for x in classes]

76

77 if not colspans or len(colspans) != n:

78 # blank, or duff (in which case ignore)

79 colspans = [""] * n

80 else:

81 colspans = [(f' colspan="{x}"' if x else "") for x in colspans]

82

83 if not colwidths or len(colwidths) != n:

84 # blank, or duff (in which case ignore)

85 colwidths = [""] * n

86 else:

87 colwidths = [(f' width="{x}"' if x else "") for x in colwidths]

88

89 celltype = "th" if heading else "td"

90 rows = "".join(

91 [

92 (

93 f"<{celltype}{classes[i]}{colspans[i]}{colwidths[i]}>"

94 f"{default if columns[i] is None else columns[i]}"

95 f"</{celltype}>"

96 )

97 for i in range(n)

98 ]

99 )

100 return f"<tr>{rows}</tr>\n"

101

102

103 def div(content: str, div_class: str = "") -> str:

104 """

105 Make simple HTML div.

106 """

107 class_str = f' class="{div_class}"' if div_class else ""

108 return f"""

109 <div{class_str}>

110 {content}

111 </div>

112 """

113

114

115 def table(content: str, table_class: str = "") -> str:

116 """

117 Make simple HTML table.

118 """

119 class_str = f' class="{table_class}"' if table_class else ""

120 return f"""

121 <table{class_str}>

122 {content}

123 </table>

124 """

125

126

127 def tr(*args: Any, tr_class: str = "", literal: bool = False) -> str:

128 """

129 Make simple HTML table data row.

130

131 Args:

132 *args: Set of columns data.

133 literal: Treat elements as literals with their own ``<td> ... </td>``,

134 rather than things to be encapsulated.

135 tr_class: table row class

136 """

137 if literal:

138 elements = args

139 else:

140 elements = [td(x) for x in args] # type: ignore[assignment]

141 tr_class = f' class="{tr_class}"' if tr_class else ""

142 contents = "".join(elements)

143 return f"<tr{tr_class}>{contents}</tr>\n"

144

145

146 def td(contents: Any, td_class: str = "", td_width: str = "") -> str:

147 """

148 Make simple HTML table data ``<td>...</td>`` cell.

149 """

150 td_class = f' class="{td_class}"' if td_class else ""

151 td_width = f' width="{td_width}"' if td_width else ""

152 return f"<td{td_class}{td_width}>{contents}</td>\n"

153

154

155 def th(contents: Any, th_class: str = "", th_width: str = "") -> str:

156 """

157 Make simple HTML table header ``<th>...</th>`` cell.

158 """

159 th_class = f' class="{th_class}"' if th_class else ""

160 th_width = f' width="{th_width}"' if th_width else ""

161 return f"<th{th_class}{th_width}>{contents}</th>\n"

162

163

164 def tr_qa(

165 q: str, a: Any, default: str = "?", default_for_blank_strings: bool = False

166) -> str:

167 """

168 Make HTML two-column data row (``<tr>...</tr>``), with the right-hand

169 column formatted as an answer.

170 """

171 return tr(

172 q,

173 answer(

174 a,

175 default=default,

176 default_for_blank_strings=default_for_blank_strings,

177 ),

178 )

179

180

181 def heading_spanning_two_columns(s: str) -> str:

182 """

183 HTML table heading row spanning 2 columns.

184 """

185 return tr_span_col(s, cols=2, tr_class=CssClass.HEADING)

186

187

188 def subheading_spanning_two_columns(s: str, th_not_td: bool = False) -> str:

189 """

190 HTML table subheading row spanning 2 columns.

191 """

192 return tr_span_col(

193 s, cols=2, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td

194 )

195

196

197 def subheading_spanning_three_columns(s: str, th_not_td: bool = False) -> str:

198 """

199 HTML table subheading row spanning 3 columns.

200 """

201 return tr_span_col(

202 s, cols=3, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td

203 )

204

205

206 def subheading_spanning_four_columns(s: str, th_not_td: bool = False) -> str:

207 """

208 HTML table subheading row spanning 4 columns.

209 """

210 return tr_span_col(

211 s, cols=4, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td

212 )

213

214

215 def bold(x: str) -> str:

216 """

217 Applies HTML bold.

218 """

219 return f"<b>{x}</b>"

220

221

222 def italic(x: str) -> str:

223 """

224 Applies HTML italic.

225 """

226 return f"<i>{x}</i>"

227

228

229 def identity(x: Any) -> Any:

230 """

231 Returns argument unchanged.

232 """

233 return x

234

235

236 def bold_webify(x: str) -> str:

237 """

238 Webifies the string, then makes it bold.

239 """

240 return bold(ws.webify(x))

241

242

243 def sub(x: str) -> str:

244 """

245 Applies HTML subscript.

246 """

247 return f"<sub>{x}</sub>"

248

249

250 def sup(x: str) -> str:

251 """

252 Applies HTML superscript.

253 """

254 return f"<sup>{x}</sup>"

255

256

257 def answer(

258 x: Any,

259 default: str = "?",

260 default_for_blank_strings: bool = False,

261 formatter_answer: Callable[[str], str] = bold_webify,

262 formatter_blank: Callable[[str], str] = italic,

263) -> str:

264 """

265 Formats answer in bold, or the default value if None.

266

267 Avoid the word "None" for the default, e.g.

268 "Score indicating likelihood of abuse: None"... may be misleading!

269 Prefer "?" instead.

270 """

271 if x is None:

272 return formatter_blank(default)

273 if default_for_blank_strings and not x and isinstance(x, str):

274 return formatter_blank(default)

275 return formatter_answer(x)

276

277

278 def tr_span_col(

279 x: str,

280 cols: int = 2,

281 tr_class: str = "",

282 td_class: str = "",

283 th_not_td: bool = False,

284) -> str:

285 """

286 HTML table data row spanning several columns.

287

288 Args:

289 x: Data.

290 cols: Number of columns to span.

291 tr_class: CSS class to apply to tr.

292 td_class: CSS class to apply to td.

293 th_not_td: make it a th, not a td.

294 """

295 cell = "th" if th_not_td else "td"

296 tr_cl = f' class="{tr_class}"' if tr_class else ""

297 td_cl = f' class="{td_class}"' if td_class else ""

298 return f'<tr{tr_cl}><{cell} colspan="{cols}"{td_cl}>{x}</{cell}></tr>'

299

300

301 def get_data_url(mimetype: str, data: Union[bytes, memoryview]) -> str:

302 """

303 Takes data (in binary format) and returns a data URL as per RFC 2397

304 (https://tools.ietf.org/html/rfc2397), such as:

305

306 .. code-block:: none

307

308 data:MIMETYPE;base64,B64_ENCODED_DATA

309 """

310 return f"data:{mimetype};base64,{base64.b64encode(data).decode('ascii')}"

311

312

313 def get_embedded_img_tag(mimetype: str, data: Union[bytes, memoryview]) -> str:

314 """

315 Takes a binary image and its MIME type, and produces an HTML tag of the

316 form:

317

318 .. code-block:: none

319

320 <img src="DATA_URL">

321 """

322 return f"<img src={get_data_url(mimetype, data)}>"

323

324

325# =============================================================================

326# Field formatting

327# =============================================================================

328

329

330 def get_yes_no(req: "CamcopsRequest", x: Any) -> str:

331 """

332 'Yes' if x else 'No'

333 """

334 return req.sstring(SS.YES) if x else req.sstring(SS.NO)

335

336

337 def get_yes_no_none(req: "CamcopsRequest", x: Any) -> Optional[str]:

338 """

339 Returns 'Yes' for True, 'No' for False, or None for None.

340 """

341 if x is None:

342 return None

343 return get_yes_no(req, x)

344

345

346 def get_yes_no_unknown(req: "CamcopsRequest", x: Any) -> str:

347 """

348 Returns 'Yes' for True, 'No' for False, or '?' for None.

349 """

350 if x is None:

351 return "?"

352 return get_yes_no(req, x)

353

354

355 def get_true_false(req: "CamcopsRequest", x: Any) -> str:

356 """

357 'True' if x else 'False'

358 """

359 return req.sstring(SS.TRUE) if x else req.sstring(SS.FALSE)

360

361

362 def get_true_false_none(req: "CamcopsRequest", x: Any) -> Optional[str]:

363 """

364 Returns 'True' for True, 'False' for False, or None for None.

365 """

366 if x is None:

367 return None

368 return get_true_false(req, x)

369

370

371 def get_true_false_unknown(req: "CamcopsRequest", x: Any) -> str:

372 """

373 Returns 'True' for True, 'False' for False, or '?' for None.

374 """

375 if x is None:

376 return "?"

377 return get_true_false(req, x)

378

379

380 def get_present_absent(req: "CamcopsRequest", x: Any) -> str:

381 """

382 'Present' if x else 'Absent'

383 """

384 return req.sstring(SS.PRESENT) if x else req.sstring(SS.ABSENT)

385

386

387 def get_present_absent_none(req: "CamcopsRequest", x: Any) -> Optional[str]:

388 """

389 Returns 'Present' for True, 'Absent' for False, or None for None.

390 """

391 if x is None:

392 return None

393 return get_present_absent(req, x)

394

395

396 def get_present_absent_unknown(req: "CamcopsRequest", x: str) -> str:

397 """

398 Returns 'Present' for True, 'Absent' for False, or '?' for None.

399 """

400 if x is None:

401 return "?"

402 return get_present_absent(req, x)

403

404

405 def get_ternary(

406 x: Any,

407 value_true: Any = True,

408 value_false: Any = False,

409 value_none: Any = None,

410) -> Any:

411 """

412 Returns ``value_none`` if ``x`` is ``None``, ``value_true`` if it's truthy,

413 or ``value_false`` if it's falsy.

414 """

415 if x is None:

416 return value_none

417 if x:

418 return value_true

419 return value_false

420

421

422 def get_correct_incorrect_none(x: Any) -> Optional[str]:

423 """

424 Returns None if ``x`` is None, "Correct" if it's truthy, or "Incorrect" if

425 it's falsy.

426 """

427 return get_ternary(x, "Correct", "Incorrect", None)

428

429

430 def pmid(x: int) -> str:

431 """

432 Returns hyperlinked text to a PubMed ID (PMID).

433

434 Args:

435 x: The integer PMID.

436

437 Returns:

438 Hyperlinked text, as raw HTML.

439

440 """

441 return f'<a href="https://pubmed.ncbi.nlm.nih.gov/{x}/">PMID {x}</a>'

442

443

444 def doi(x: str) -> str:

445 """

446 Returns hyperlinked text to a digital object identifier (DOI).

447

448 Args:

449 x: The DOI.

450

451 Returns:

452 Hyperlinked text, as raw HTML.

453

454 """

455 return f'<a href="https://doi.org/{x}">doi:{x}</a>'

456

457

458 def a_href(url: str, text: str = None) -> str:

459 """

460 Returns text hyperlinked to a URL; by default, the text is the URL itself.

461

462 Args:

463 url: the raw URL

464 text: text to be shown

465 """

466 text = text or url

467 return f'<a href="{url}">{text}</a>'