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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/cc_modules/cc_html.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**Basic HTML creation functions.**
28"""
30import base64
31from typing import Any, Callable, List, Optional, TYPE_CHECKING, Union
33import cardinal_pythonlib.rnc_web as ws
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_text import SS
38if TYPE_CHECKING:
39 from camcops_server.cc_modules.cc_request import CamcopsRequest
42# =============================================================================
43# HTML elements
44# =============================================================================
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.
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?
66 Returns:
67 the ``<tr>...</tr>`` string
68 """
69 n = len(columns)
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]
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]
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]
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"
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 """
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 """
127def tr(*args: Any, tr_class: str = "", literal: bool = False) -> str:
128 """
129 Make simple HTML table data row.
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"
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"
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"
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 )
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)
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 )
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 )
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 )
215def bold(x: str) -> str:
216 """
217 Applies HTML bold.
218 """
219 return f"<b>{x}</b>"
222def italic(x: str) -> str:
223 """
224 Applies HTML italic.
225 """
226 return f"<i>{x}</i>"
229def identity(x: Any) -> Any:
230 """
231 Returns argument unchanged.
232 """
233 return x
236def bold_webify(x: str) -> str:
237 """
238 Webifies the string, then makes it bold.
239 """
240 return bold(ws.webify(x))
243def sub(x: str) -> str:
244 """
245 Applies HTML subscript.
246 """
247 return f"<sub>{x}</sub>"
250def sup(x: str) -> str:
251 """
252 Applies HTML superscript.
253 """
254 return f"<sup>{x}</sup>"
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.
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)
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.
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>'
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:
306 .. code-block:: none
308 data:MIMETYPE;base64,B64_ENCODED_DATA
309 """
310 return f"data:{mimetype};base64,{base64.b64encode(data).decode('ascii')}"
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:
318 .. code-block:: none
320 <img src="DATA_URL">
321 """
322 return f"<img src={get_data_url(mimetype, data)}>"
325# =============================================================================
326# Field formatting
327# =============================================================================
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)
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)
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)
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)
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)
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)
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)
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)
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)
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
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)
430def pmid(x: int) -> str:
431 """
432 Returns hyperlinked text to a PubMed ID (PMID).
434 Args:
435 x: The integer PMID.
437 Returns:
438 Hyperlinked text, as raw HTML.
440 """
441 return f'<a href="https://pubmed.ncbi.nlm.nih.gov/{x}/">PMID {x}</a>'
444def doi(x: str) -> str:
445 """
446 Returns hyperlinked text to a digital object identifier (DOI).
448 Args:
449 x: The DOI.
451 Returns:
452 Hyperlinked text, as raw HTML.
454 """
455 return f'<a href="https://doi.org/{x}">doi:{x}</a>'
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.
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>'