Coverage for cc_modules/cc_html.py: 35%

124 statements  

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

1""" 

2camcops_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 

30import base64 

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

32 

33import cardinal_pythonlib.rnc_web as ws 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_text import SS 

37 

38if TYPE_CHECKING: 

39 from camcops_server.cc_modules.cc_request import CamcopsRequest 

40 

41 

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

43# HTML elements 

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

45 

46 

47def 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 

103def 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 

115def 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 

127def 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 

146def 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 

155def 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 

164def 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 

181def 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 

188def 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 

197def 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 

206def 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 

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

216 """ 

217 Applies HTML bold. 

218 """ 

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

220 

221 

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

223 """ 

224 Applies HTML italic. 

225 """ 

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

227 

228 

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

230 """ 

231 Returns argument unchanged. 

232 """ 

233 return x 

234 

235 

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

237 """ 

238 Webifies the string, then makes it bold. 

239 """ 

240 return bold(ws.webify(x)) 

241 

242 

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

244 """ 

245 Applies HTML subscript. 

246 """ 

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

248 

249 

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

251 """ 

252 Applies HTML superscript. 

253 """ 

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

255 

256 

257def 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 

278def 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 

301def 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 

313def 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 

330def 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 

337def 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 

346def 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 

355def 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 

362def 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 

371def 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 

380def 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 

387def 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 

396def 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 

405def 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 

422def 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 

430def 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 

444def 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 

458def 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>'