Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_html.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

27**Basic HTML creation functions.** 

28 

29""" 

30 

31import base64 

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

33 

34import cardinal_pythonlib.rnc_web as ws 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_text import SS 

38 

39if TYPE_CHECKING: 

40 from camcops_server.cc_modules.cc_request import CamcopsRequest 

41 

42 

43# ============================================================================= 

44# HTML elements 

45# ============================================================================= 

46 

47def table_row(columns: List[str], 

48 classes: List[str] = None, 

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

50 colwidths: List[str] = None, 

51 default: str = "", 

52 heading: bool = False) -> str: 

53 """ 

54 Make HTML table row. 

55 

56 Args: 

57 columns: contents of HTML table columns 

58 classes: optional CSS classes, one for each column 

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

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

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

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

63 

64 Returns: 

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

66 """ 

67 n = len(columns) 

68 

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

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

71 classes = [""] * n 

72 else: 

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

74 

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

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

77 colspans = [""] * n 

78 else: 

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

80 

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

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

83 colwidths = [""] * n 

84 else: 

85 colwidths = [ 

86 (f' width="{x}"' if x else '') 

87 for x in colwidths 

88 ] 

89 

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

91 rows = "".join([ 

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 return f"<tr>{rows}</tr>\n" 

100 

101 

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

103 """ 

104 Make simple HTML div. 

105 """ 

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

107 return f""" 

108 <div{class_str}> 

109 {content} 

110 </div> 

111 """ 

112 

113 

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

115 """ 

116 Make simple HTML table. 

117 """ 

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

119 return f""" 

120 <table{class_str}> 

121 {content} 

122 </table> 

123 """ 

124 

125 

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

127 """ 

128 Make simple HTML table data row. 

129 

130 Args: 

131 *args: Set of columns data. 

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

133 rather than things to be encapsulated. 

134 tr_class: table row class 

135 """ 

136 if literal: 

137 elements = args 

138 else: 

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

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

141 contents = "".join(elements) 

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

143 

144 

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

146 """ 

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

148 """ 

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

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

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

152 

153 

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

155 """ 

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

157 """ 

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

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

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

161 

162 

163def tr_qa(q: str, 

164 a: Any, 

165 default: str = "?", 

166 default_for_blank_strings: bool = False) -> 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(q, answer(a, default=default, 

172 default_for_blank_strings=default_for_blank_strings)) 

173 

174 

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

176 """ 

177 HTML table heading row spanning 2 columns. 

178 """ 

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

180 

181 

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

183 """ 

184 HTML table subheading row spanning 2 columns. 

185 """ 

186 return tr_span_col(s, cols=2, tr_class=CssClass.SUBHEADING, 

187 th_not_td=th_not_td) 

188 

189 

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

191 """ 

192 HTML table subheading row spanning 3 columns. 

193 """ 

194 return tr_span_col(s, cols=3, tr_class=CssClass.SUBHEADING, 

195 th_not_td=th_not_td) 

196 

197 

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

199 """ 

200 HTML table subheading row spanning 4 columns. 

201 """ 

202 return tr_span_col(s, cols=4, tr_class=CssClass.SUBHEADING, 

203 th_not_td=th_not_td) 

204 

205 

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

207 """ 

208 Applies HTML bold. 

209 """ 

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

211 

212 

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

214 """ 

215 Applies HTML italic. 

216 """ 

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

218 

219 

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

221 """ 

222 Returns argument unchanged. 

223 """ 

224 return x 

225 

226 

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

228 """ 

229 Webifies the string, then makes it bold. 

230 """ 

231 return bold(ws.webify(x)) 

232 

233 

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

235 """ 

236 Applies HTML subscript. 

237 """ 

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

239 

240 

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

242 """ 

243 Applies HTML superscript. 

244 """ 

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

246 

247 

248def answer(x: Any, 

249 default: str = "?", 

250 default_for_blank_strings: bool = False, 

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

252 formatter_blank: Callable[[str], str] = italic) -> str: 

253 """ 

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

255 

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

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

258 Prefer "?" instead. 

259 """ 

260 if x is None: 

261 return formatter_blank(default) 

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

263 return formatter_blank(default) 

264 return formatter_answer(x) 

265 

266 

267def tr_span_col(x: str, 

268 cols: int = 2, 

269 tr_class: str = "", 

270 td_class: str = "", 

271 th_not_td: bool = False) -> str: 

272 """ 

273 HTML table data row spanning several columns. 

274 

275 Args: 

276 x: Data. 

277 cols: Number of columns to span. 

278 tr_class: CSS class to apply to tr. 

279 td_class: CSS class to apply to td. 

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

281 """ 

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

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

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

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

286 

287 

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

289 """ 

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

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

292 

293 .. code-block:: none 

294 

295 data:MIMETYPE;base64,B64_ENCODED_DATA 

296 """ 

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

298 

299 

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

301 """ 

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

303 form: 

304 

305 .. code-block:: none 

306 

307 <img src="DATA_URL"> 

308 """ 

309 return f'<img src={get_data_url(mimetype, data)}>' 

310 

311 

312# ============================================================================= 

313# Field formatting 

314# ============================================================================= 

315 

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

317 """ 

318 'Yes' if x else 'No' 

319 """ 

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

321 

322 

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

324 """ 

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

326 """ 

327 if x is None: 

328 return None 

329 return get_yes_no(req, x) 

330 

331 

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

333 """ 

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

335 """ 

336 if x is None: 

337 return "?" 

338 return get_yes_no(req, x) 

339 

340 

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

342 """ 

343 'True' if x else 'False' 

344 """ 

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

346 

347 

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

349 """ 

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

351 """ 

352 if x is None: 

353 return None 

354 return get_true_false(req, x) 

355 

356 

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

358 """ 

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

360 """ 

361 if x is None: 

362 return "?" 

363 return get_true_false(req, x) 

364 

365 

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

367 """ 

368 'Present' if x else 'Absent' 

369 """ 

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

371 

372 

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

374 """ 

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

376 """ 

377 if x is None: 

378 return None 

379 return get_present_absent(req, x) 

380 

381 

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

383 """ 

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

385 """ 

386 if x is None: 

387 return "?" 

388 return get_present_absent(req, x) 

389 

390 

391def get_ternary(x: Any, 

392 value_true: Any = True, 

393 value_false: Any = False, 

394 value_none: Any = None) -> Any: 

395 """ 

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

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

398 """ 

399 if x is None: 

400 return value_none 

401 if x: 

402 return value_true 

403 return value_false 

404 

405 

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

407 """ 

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

409 it's falsy. 

410 """ 

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