Coverage for cc_modules/cc_html.py: 33%

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

29 

30""" 

31 

32import base64 

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

34 

35import cardinal_pythonlib.rnc_web as ws 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_text import SS 

39 

40if TYPE_CHECKING: 

41 from camcops_server.cc_modules.cc_request import CamcopsRequest 

42 

43 

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

45# HTML elements 

46# ============================================================================= 

47 

48 

49def table_row( 

50 columns: List[str], 

51 classes: List[str] = None, 

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

53 colwidths: List[str] = None, 

54 default: str = "", 

55 heading: bool = False, 

56) -> str: 

57 """ 

58 Make HTML table row. 

59 

60 Args: 

61 columns: contents of HTML table columns 

62 classes: optional CSS classes, one for each column 

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

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

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

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

67 

68 Returns: 

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

70 """ 

71 n = len(columns) 

72 

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

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

75 classes = [""] * n 

76 else: 

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

78 

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

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

81 colspans = [""] * n 

82 else: 

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

84 

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

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

87 colwidths = [""] * n 

88 else: 

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

90 

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

92 rows = "".join( 

93 [ 

94 ( 

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

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

97 f"</{celltype}>" 

98 ) 

99 for i in range(n) 

100 ] 

101 ) 

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

103 

104 

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

106 """ 

107 Make simple HTML div. 

108 """ 

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

110 return f""" 

111 <div{class_str}> 

112 {content} 

113 </div> 

114 """ 

115 

116 

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

118 """ 

119 Make simple HTML table. 

120 """ 

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

122 return f""" 

123 <table{class_str}> 

124 {content} 

125 </table> 

126 """ 

127 

128 

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

130 """ 

131 Make simple HTML table data row. 

132 

133 Args: 

134 *args: Set of columns data. 

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

136 rather than things to be encapsulated. 

137 tr_class: table row class 

138 """ 

139 if literal: 

140 elements = args 

141 else: 

142 elements = [td(x) for x in args] 

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

144 contents = "".join(elements) 

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

146 

147 

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

149 """ 

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

151 """ 

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

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

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

155 

156 

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

158 """ 

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

160 """ 

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

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

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

164 

165 

166def tr_qa( 

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

168) -> str: 

169 """ 

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

171 column formatted as an answer. 

172 """ 

173 return tr( 

174 q, 

175 answer( 

176 a, 

177 default=default, 

178 default_for_blank_strings=default_for_blank_strings, 

179 ), 

180 ) 

181 

182 

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

184 """ 

185 HTML table heading row spanning 2 columns. 

186 """ 

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

188 

189 

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

191 """ 

192 HTML table subheading row spanning 2 columns. 

193 """ 

194 return tr_span_col( 

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

196 ) 

197 

198 

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

200 """ 

201 HTML table subheading row spanning 3 columns. 

202 """ 

203 return tr_span_col( 

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

205 ) 

206 

207 

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

209 """ 

210 HTML table subheading row spanning 4 columns. 

211 """ 

212 return tr_span_col( 

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

214 ) 

215 

216 

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

218 """ 

219 Applies HTML bold. 

220 """ 

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

222 

223 

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

225 """ 

226 Applies HTML italic. 

227 """ 

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

229 

230 

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

232 """ 

233 Returns argument unchanged. 

234 """ 

235 return x 

236 

237 

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

239 """ 

240 Webifies the string, then makes it bold. 

241 """ 

242 return bold(ws.webify(x)) 

243 

244 

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

246 """ 

247 Applies HTML subscript. 

248 """ 

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

250 

251 

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

253 """ 

254 Applies HTML superscript. 

255 """ 

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

257 

258 

259def answer( 

260 x: Any, 

261 default: str = "?", 

262 default_for_blank_strings: bool = False, 

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

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

265) -> str: 

266 """ 

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

268 

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

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

271 Prefer "?" instead. 

272 """ 

273 if x is None: 

274 return formatter_blank(default) 

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

276 return formatter_blank(default) 

277 return formatter_answer(x) 

278 

279 

280def tr_span_col( 

281 x: str, 

282 cols: int = 2, 

283 tr_class: str = "", 

284 td_class: str = "", 

285 th_not_td: bool = False, 

286) -> str: 

287 """ 

288 HTML table data row spanning several columns. 

289 

290 Args: 

291 x: Data. 

292 cols: Number of columns to span. 

293 tr_class: CSS class to apply to tr. 

294 td_class: CSS class to apply to td. 

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

296 """ 

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

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

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

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

301 

302 

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

304 """ 

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

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

307 

308 .. code-block:: none 

309 

310 data:MIMETYPE;base64,B64_ENCODED_DATA 

311 """ 

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

313 

314 

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

316 """ 

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

318 form: 

319 

320 .. code-block:: none 

321 

322 <img src="DATA_URL"> 

323 """ 

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

325 

326 

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

328# Field formatting 

329# ============================================================================= 

330 

331 

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

333 """ 

334 'Yes' if x else 'No' 

335 """ 

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

337 

338 

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

340 """ 

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

342 """ 

343 if x is None: 

344 return None 

345 return get_yes_no(req, x) 

346 

347 

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

349 """ 

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

351 """ 

352 if x is None: 

353 return "?" 

354 return get_yes_no(req, x) 

355 

356 

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

358 """ 

359 'True' if x else 'False' 

360 """ 

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

362 

363 

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

365 """ 

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

367 """ 

368 if x is None: 

369 return None 

370 return get_true_false(req, x) 

371 

372 

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

374 """ 

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

376 """ 

377 if x is None: 

378 return "?" 

379 return get_true_false(req, x) 

380 

381 

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

383 """ 

384 'Present' if x else 'Absent' 

385 """ 

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

387 

388 

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

390 """ 

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

392 """ 

393 if x is None: 

394 return None 

395 return get_present_absent(req, x) 

396 

397 

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

399 """ 

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

401 """ 

402 if x is None: 

403 return "?" 

404 return get_present_absent(req, x) 

405 

406 

407def get_ternary( 

408 x: Any, 

409 value_true: Any = True, 

410 value_false: Any = False, 

411 value_none: Any = None, 

412) -> Any: 

413 """ 

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

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

416 """ 

417 if x is None: 

418 return value_none 

419 if x: 

420 return value_true 

421 return value_false 

422 

423 

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

425 """ 

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

427 it's falsy. 

428 """ 

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